#!/usr/bin/env python3
"""
Video Viewer Bot - Browse and search generated videos.
Independent Telegram bot with Claude-powered search.

Now also includes admin commands for token management.
"""

import asyncio
import json
import logging
import os
import sys
from collections import OrderedDict
from datetime import datetime, timezone, timedelta
from pathlib import Path

from dotenv import load_dotenv
import telegram
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup, BotCommand
from telegram.error import NetworkError
from telegram.ext import (
    Application,
    CommandHandler,
    ContextTypes,
    MessageHandler,
    CallbackQueryHandler,
    filters,
)

import data_reader
import search
import session

# Add scripts directory to path for database imports
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from database import (
    init_db, get_db_path, get_connection, get_user, get_or_create_user, get_all_users,
    get_users_count, search_users, get_recent_transactions, count_transactions,
    get_promo_remaining, set_promo_remaining, set_user_promo_eligible, get_stats,
    get_ref_stats, get_user_generations,
    # Showcase functions
    init_showcase_db, add_showcase_video, remove_showcase_video,
    get_showcase_video, get_showcase_video_by_task, get_showcase_videos,
    get_showcase_videos_count, set_showcase_video_active,
    set_showcase_video_artifacts, set_showcase_video_rating,
    # Gift pack functions
    create_gift_pack, get_user_active_packs, get_user_packs,
    get_gift_pack_stats, set_gift_pack_received,
    create_gift_pack_campaign, get_all_campaigns, deactivate_campaign,
    get_inactive_users,
)
from token_manager import (
    get_balance, credit_tokens, revoke_tokens, block_user, unblock_user,
    get_user_info, is_blocked
)
from survey_db import (
    create_survey, get_survey, get_active_surveys, deactivate_survey,
    create_invitation, get_invitation, get_survey_stats,
    get_survey_target_users, get_all_responses_export, DEFAULT_QUESTIONS,
)
from survey_handler import send_survey_invite

# Load environment
load_dotenv()

BOT_TOKEN = os.getenv("VIEWER_BOT_TOKEN")
ALLOWED_USERS = set(map(int, os.getenv("ALLOWED_USERS", "").split(","))) if os.getenv("ALLOWED_USERS") else set()
VIEWER_ONLY_USERS = set(map(int, os.getenv("VIEWER_ONLY_USERS", "").split(","))) if os.getenv("VIEWER_ONLY_USERS") else set()
ALL_ALLOWED = ALLOWED_USERS | VIEWER_ONLY_USERS

# Admin users who can manage tokens (subset of ALLOWED_USERS)
ADMIN_USERS = set(map(int, os.getenv("ADMIN_USERS", "").split(","))) if os.getenv("ADMIN_USERS") else ALLOWED_USERS

# Main bot tokens (for sending gift videos via the main bot)
MAIN_BOT_TOKEN_DEV = os.getenv("MAIN_BOT_TOKEN_DEV", "")
MAIN_BOT_TOKEN_PROD = os.getenv("MAIN_BOT_TOKEN_PROD", "")
MAIN_BOT_USERNAME_DEV = "your_test_bot"
MAIN_BOT_USERNAME_PROD = "YourMainBot"
TG_PROXY = os.getenv("TG_PROXY", "")

# Database paths for dev/prod
NFS_BASE = Path(os.getenv("NFS_BASE", "/mnt/video"))
DB_PATH_DEV = NFS_BASE / "videobot_dev.db"
DB_PATH_PROD = NFS_BASE / "videobot_prod.db"

# Current database environment (per-user state stored in context.user_data)
DEFAULT_DB_ENV = "dev"

# Logging
logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    level=logging.INFO
)
logger = logging.getLogger(__name__)

ITEMS_PER_PAGE = 10

# Video URL base for web viewing
VIDEO_BASE_URL = "https://your-server.example.com"


def get_video_url(product_id: str, task_id: str) -> str:
    """Get web URL for viewing a video. Finds the correct marketplace prefix on disk."""
    for pfx in ("wb", "avito", "ozon"):
        if (NFS_BASE / f"{pfx}_{product_id}" / task_id).is_dir():
            return f"{VIDEO_BASE_URL}/{pfx}_{product_id}/{task_id}/videos/video.mp4"
    # Fallback to wb for backward compat
    return f"{VIDEO_BASE_URL}/wb_{product_id}/{task_id}/videos/video.mp4"


def is_authorized(user_id: int) -> bool:
    """Check if user is authorized."""
    return user_id in ALL_ALLOWED


async def unauthorized_response(update: Update):
    """Send unauthorized message."""
    msg = update.message or update.callback_query.message
    await msg.reply_text(
        "⛔ Доступ запрещён. Этот бот только для авторизованных пользователей."
    )


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /start command."""
    user_id = update.effective_user.id

    if not is_authorized(user_id):
        await unauthorized_response(update)
        return

    data_reader.log_viewer_action(user_id, "START", "")

    help_text = """🎬 *Video Viewer Bot*

Просмотр и поиск сгенерированных видео.

*Команды:*
/products — Список товаров
/product <id> — Детали товара
/video <task\\_id> — Просмотр видео
/stats — Статистика

*Поиск:*
Просто отправьте текстовое сообщение для поиска.
Примеры:
• "хорошие видео косметики"
• "видео с ошибками"
• "удобрения"
"""
    keyboard = [
        [InlineKeyboardButton("📦 Товары", callback_data="products:1"),
         InlineKeyboardButton("📊 Статистика", callback_data="stats")]
    ]
    await update.message.reply_text(
        help_text,
        reply_markup=InlineKeyboardMarkup(keyboard)
    )


async def products(update: Update, context: ContextTypes.DEFAULT_TYPE, page: int = 1):
    """Handle /products command - list all products."""
    user_id = update.effective_user.id

    if not is_authorized(user_id):
        await unauthorized_response(update)
        return

    data_reader.log_viewer_action(user_id, "PRODUCTS", str(page))

    # Get page from args if command
    if context.args:
        try:
            page = int(context.args[0])
        except ValueError:
            pass

    products_list = data_reader.list_products()
    total = len(products_list)
    total_pages = (total + ITEMS_PER_PAGE - 1) // ITEMS_PER_PAGE
    page = max(1, min(page, total_pages))

    start_idx = (page - 1) * ITEMS_PER_PAGE
    end_idx = start_idx + ITEMS_PER_PAGE
    page_products = products_list[start_idx:end_idx]

    lines = [f"📦 Товары (стр. {page}/{total_pages}, всего {total}):\n"]

    for p in page_products:
        name = p.product_name[:25] + "..." if p.product_name and len(p.product_name) > 25 else (p.product_name or "—")
        good = p.ratings.get("good", 0)
        bad = p.ratings.get("bad", 0)
        rating_str = f" 👍{good} 👎{bad}" if good or bad else ""

        # Tappable command link
        lines.append(f"🎬 {p.successful_count}/{p.generation_count}{rating_str} {name}")
        lines.append(f"  /p_{p.source}_{p.product_id}")
        lines.append("")

    # Build pagination keyboard
    nav_buttons = []
    if page > 1:
        nav_buttons.append(InlineKeyboardButton("⬅️", callback_data=f"products:{page-1}"))
    nav_buttons.append(InlineKeyboardButton(f"{page}/{total_pages}", callback_data="noop"))
    if page < total_pages:
        nav_buttons.append(InlineKeyboardButton("➡️", callback_data=f"products:{page+1}"))

    keyboard = [nav_buttons]

    msg = update.message or update.callback_query.message
    text = "\n".join(lines)

    if update.callback_query:
        await msg.edit_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
    else:
        await msg.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard))


async def product(update: Update, context: ContextTypes.DEFAULT_TYPE, product_id: str = None):
    """Handle /product <id> command - show product details with latest video."""
    user_id = update.effective_user.id

    if not is_authorized(user_id):
        await unauthorized_response(update)
        return

    # Get product_id and optional source from args
    source = None
    if context.args:
        product_id = context.args[0]
        if len(context.args) >= 2:
            source = context.args[1]

    if not product_id:
        msg = update.message or update.callback_query.message
        await msg.reply_text("Использование: /product <product_id>")
        return

    data_reader.log_viewer_action(user_id, "PRODUCT", product_id)

    generations = data_reader.get_product_generations(product_id, source=source)

    if not generations:
        msg = update.message or update.callback_query.message
        await msg.reply_text(f"Товар {product_id} не найден или нет генераций.")
        return

    # Update session
    user_session = session.load_session(user_id)
    session.set_last_product(user_session, product_id)

    product_name = generations[0].product_name or "—"
    video_gens = [g for g in generations if g.has_video]

    # Determine marketplace source from first generation's directory
    gen_source = source or generations[0].source if hasattr(generations[0], 'source') else None
    if not gen_source:
        for pfx in ("wb", "avito", "ozon"):
            if (Path(f"/mnt/video/{pfx}_{product_id}")).exists():
                gen_source = pfx
                break
    wb_url = ""
    if gen_source == "wb" or not gen_source:
        wb_url = f"\nhttps://www.wildberries.ru/catalog/{product_id}/detail.aspx"

    lines = [f"📦 Товар {product_id}"]
    lines.append(f"{product_name}{wb_url}\n")
    lines.append(f"Генераций: {len(generations)} (🎬 {len(video_gens)} видео)\n")

    # Show generations with tappable links and feedback
    for gen in generations[:10]:
        emoji = "🎬" if gen.has_video else "❌"
        rating = ""
        if gen.rating == "good":
            rating = " 👍"
        elif gen.rating == "bad":
            rating = " 👎"

        date_str = gen.started_at.strftime("%d.%m %H:%M") if gen.started_at else "—"
        if gen.has_video:
            lines.append(f"{emoji} {date_str}{rating} /v_{gen.task_id}")
        else:
            lines.append(f"{emoji} {date_str}{rating} {gen.task_id}")

        # Show comments if exist
        if gen.comments:
            for c in gen.comments:
                lines.append(f"   💬 {c}")

    if len(generations) > 10:
        lines.append(f"\n... и ещё {len(generations) - 10}")

    # Navigation keyboard
    keyboard = [[InlineKeyboardButton("⬅️ К товарам", callback_data="products:1")]]

    # If there are videos, add buttons for first few
    if video_gens:
        video_buttons = []
        for i, g in enumerate(video_gens[:4]):
            video_buttons.append(InlineKeyboardButton(f"🎬 {i+1}", callback_data=f"video:{g.task_id}"))
        keyboard.insert(0, video_buttons)

    msg = update.message or update.callback_query.message
    text = "\n".join(lines)

    if update.callback_query:
        await msg.edit_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
    else:
        await msg.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard))


def build_video_keyboard(task_id: str, product_id: str, showcase_entry: dict = None):
    """Build keyboard for video view (reusable for showcase callbacks)."""
    keyboard = [
        [
            InlineKeyboardButton("👍 Good", callback_data=f"rate:{task_id}:good"),
            InlineKeyboardButton("👎 Bad", callback_data=f"rate:{task_id}:bad")
        ],
    ]

    # Showcase toggle button (admin only, but we'll check in callback)
    if showcase_entry:
        showcase_btn_text = f"➖ Из портфолио (ID:{showcase_entry['id']})"
        keyboard.append([
            InlineKeyboardButton(showcase_btn_text, callback_data=f"showcase_remove:{task_id}")
        ])
        # Rating buttons for showcase
        current_rating = showcase_entry.get("rating", 0)
        rating_row = []
        for r in range(1, 6):
            star = "★" if r <= current_rating else "☆"
            rating_row.append(InlineKeyboardButton(star, callback_data=f"showcase_rate:{task_id}:{r}"))
        keyboard.append(rating_row)
    else:
        keyboard.append([
            InlineKeyboardButton("➕ В портфолио", callback_data=f"showcase_add:{task_id}")
        ])

    keyboard.append([
        InlineKeyboardButton(f"📦 Товар {product_id}", callback_data=f"product:{product_id}"),
        InlineKeyboardButton("⬅️ К товарам", callback_data="products:1")
    ])

    return keyboard


async def video(update: Update, context: ContextTypes.DEFAULT_TYPE, task_id: str = None):
    """Handle /video <task_id> command - show video with metadata."""
    user_id = update.effective_user.id

    if not is_authorized(user_id):
        await unauthorized_response(update)
        return

    # Get task_id from args if command
    if context.args:
        task_id = context.args[0]

    if not task_id:
        msg = update.message or update.callback_query.message
        await msg.reply_text("Использование: /video <task_id>")
        return

    data_reader.log_viewer_action(user_id, "VIDEO", task_id)

    gen = data_reader.get_generation(task_id)

    if not gen:
        msg = update.message or update.callback_query.message
        await msg.reply_text(f"Генерация {task_id} не найдена.")
        return

    # Update session
    user_session = session.load_session(user_id)
    session.add_viewed_video(user_session, task_id)

    # Build info message
    lines = [f"🎬 {task_id}\n"]

    if gen.product_name:
        lines.append(f"📦 {gen.product_name}")

    lines.append(f"Товар: {gen.product_id}")

    if gen.started_at:
        lines.append(f"Создано: {gen.started_at.strftime('%d.%m.%Y %H:%M')}")

    # Rating and feedback
    if gen.rating:
        emoji = "👍" if gen.rating == "good" else "👎"
        lines.append(f"\n⭐ Оценка: {emoji}")

    if gen.comments:
        lines.append("💬 Комментарии:")
        for c in gen.comments:
            lines.append(f"  {c}")

    if gen.errors:
        lines.append(f"\n⚠️ Ошибки: {len(gen.errors)}")
        for e in gen.errors[:3]:
            lines.append(f"  • {str(e)[:100]}")

    # Story vision details
    if gen.story_vision:
        sv = gen.story_vision
        lines.append(f"\n═══ STORY VISION ═══")
        lines.append(f"🎭 Архетип: {sv.get('archetype', '—')}")
        lines.append(f"📂 Категория: {sv.get('category', '—')}")

        # Act 1
        act1 = sv.get('act1', {})
        if act1:
            lines.append(f"\n🎬 ACT 1 ({act1.get('duration_sec', '?')}s):")
            lines.append(f"  {act1.get('scene', '—')}")
            lines.append(f"  Эмоция: {act1.get('emotion', '—')}")

        # Transition
        if sv.get('transition'):
            lines.append(f"\n↔️ Переход: {sv.get('transition')}")

        # Act 2
        act2 = sv.get('act2', {})
        if act2:
            lines.append(f"\n🎬 ACT 2 ({act2.get('duration_sec', '?')}s):")
            lines.append(f"  {act2.get('scene', '—')}")
            lines.append(f"  Эмоция: {act2.get('emotion', '—')}")

    # Frame prompts
    if gen.frame_prompts:
        lines.append(f"\n═══ FRAME PROMPTS ═══")
        if gen.frame_prompts.get('frame_act1'):
            prompt = gen.frame_prompts['frame_act1'][:500]
            lines.append(f"\n📷 Act 1:\n{prompt}...")
        if gen.frame_prompts.get('frame_act2'):
            prompt = gen.frame_prompts['frame_act2'][:500]
            lines.append(f"\n📷 Act 2:\n{prompt}...")

    # Veo3 prompt
    if gen.veo3_prompt:
        lines.append(f"\n═══ VEO3 PROMPT ═══")
        lines.append(gen.veo3_prompt[:800])

    # Check if video is in showcase
    showcase_entry = get_showcase_video_by_task(task_id)

    # Build keyboard using helper function
    keyboard = build_video_keyboard(task_id, gen.product_id, showcase_entry)

    msg = update.message or update.callback_query.message
    text = "\n".join(lines)

    # Add web link
    video_url = get_video_url(gen.product_id, task_id)
    text += f"\n\n🔗 {video_url}"

    # For callback, edit message then send video separately
    if update.callback_query:
        await msg.edit_text(text, reply_markup=InlineKeyboardMarkup(keyboard))

    else:
        await msg.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard))

    # Send video via local file upload (URL times out - Telegram cuts connection at ~1MB)
    if gen.has_video and gen.video_path:
        loading_msg = await msg.reply_text("📤 Отправляю видео...")
        try:
            if TG_PROXY:
                import httpx
                bot_token = BOT_TOKEN
                url = f"https://api.telegram.org/bot{bot_token}/sendVideo"
                data = {"chat_id": str(msg.chat_id), "caption": f"🎬 {task_id}",
                        "supports_streaming": "true"}
                with open(gen.video_path, "rb") as f:
                    files = {"video": (Path(gen.video_path).name, f, "video/mp4")}
                    async with httpx.AsyncClient(proxy=TG_PROXY, timeout=600) as client:
                        resp = await client.post(url, data=data, files=files)
                if resp.status_code != 200:
                    raise Exception(f"Proxy send failed: HTTP {resp.status_code}")
            else:
                with open(gen.video_path, "rb") as f:
                    await msg.reply_video(f, caption=f"🎬 {task_id}", supports_streaming=True,
                                          read_timeout=300, write_timeout=300)
            await loading_msg.delete()
        except Exception as e:
            logger.error(f"Failed to send video from local file: {e}")
            await loading_msg.edit_text(f"🎬 Не удалось отправить видео: {e}")
    else:
        await msg.reply_text("❌ Видео не найдено")


async def stats(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /stats command - show global statistics."""
    user_id = update.effective_user.id

    if not is_authorized(user_id):
        await unauthorized_response(update)
        return

    data_reader.log_viewer_action(user_id, "STATS", "")

    s = data_reader.get_stats()

    text = f"""📊 *Статистика*

📦 Товаров: {s['products_count']}
🎬 Всего генераций: {s['total_generations']}
✅ Успешных: {s['total_successful']}
❌ С ошибками: {s['total_failed']}
📈 Успешность: {s['success_rate']}%

*Оценки:*
👍 Хорошие: {s['ratings']['good']}
👎 Плохие: {s['ratings']['bad']}
"""
    keyboard = [[InlineKeyboardButton("📦 Товары", callback_data="products:1")]]

    msg = update.message or update.callback_query.message
    if update.callback_query:
        await msg.edit_text(text, reply_markup=InlineKeyboardMarkup(keyboard))
    else:
        await msg.reply_text(text, reply_markup=InlineKeyboardMarkup(keyboard))


async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle all callback queries from inline buttons."""
    query = update.callback_query
    await query.answer()

    user_id = update.effective_user.id
    if not is_authorized(user_id):
        await unauthorized_response(update)
        return

    data = query.data

    if data == "noop":
        return

    if data.startswith("products:"):
        page = int(data.split(":")[1])
        await products(update, context, page=page)

    elif data.startswith("product:"):
        product_id = data.split(":")[1]
        await product(update, context, product_id=product_id)

    elif data.startswith("video:"):
        task_id = data.split(":")[1]
        await video(update, context, task_id=task_id)

    elif data.startswith("rate:"):
        parts = data.split(":")
        task_id = parts[1]
        rating = parts[2]

        if data_reader.write_feedback(task_id, user_id, "RATE", rating):
            emoji = "👍" if rating == "good" else "👎"
            await query.answer(f"{emoji} Оценка сохранена!", show_alert=True)
            data_reader.log_viewer_action(user_id, "RATE", f"{task_id}:{rating}")
        else:
            await query.answer("❌ Ошибка сохранения", show_alert=True)

    elif data == "stats":
        await stats(update, context)


async def search_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /search command."""
    user_id = update.effective_user.id

    if not is_authorized(user_id):
        await unauthorized_response(update)
        return

    if not context.args:
        await update.message.reply_text("Использование: /search <запрос>")
        return

    query = " ".join(context.args)
    await do_search(update, user_id, query)


def escape_md(text: str) -> str:
    """Escape markdown special characters."""
    if not text:
        return text
    for char in ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']:
        text = text.replace(char, '\\' + char)
    return text


async def do_search(update: Update, user_id: int, query: str):
    """Perform search and send results."""
    data_reader.log_viewer_action(user_id, "SEARCH", query)

    # Get user context
    user_session = session.load_session(user_id)
    context_summary = session.get_context_summary(user_session)

    await update.message.reply_text(f"🔍 Ищу: {query}...")

    results = search.search_videos(query, context_summary)

    # Save to session
    session.add_search(user_session, query, [r.task_id for r in results])
    session.add_to_context(user_session, "user", query)

    # Build response grouped by product
    if not results:
        await update.message.reply_text("Ничего не найдено по вашему запросу.")
        return

    # Group by (source, product_id)
    by_product = {}
    for r in results:
        key = (r.source, r.product_id)
        if key not in by_product:
            by_product[key] = {
                "product_name": r.product_name,
                "videos": []
            }
        by_product[key]["videos"].append(r)

    lines = [f"Найдено {len(results)} видео в {len(by_product)} товарах:\n"]

    for (source, product_id), data in by_product.items():
        name = data["product_name"]
        if name and len(name) > 40:
            name = name[:40] + "..."
        name = name or "—"

        lines.append(f"📦 /p_{source}_{product_id}")
        lines.append(f"   {name}")

        for v in data["videos"]:
            emoji = "🎬" if v.has_video else "❌"
            lines.append(f"   {emoji} /v_{v.task_id}")
            if v.reason:
                lines.append(f"      {v.reason}")

        lines.append("")

    session.add_to_context(user_session, "assistant", f"Found {len(results)} results")

    await update.message.reply_text("\n".join(lines))


async def rate(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /rate command."""
    user_id = update.effective_user.id

    if not is_authorized(user_id):
        await unauthorized_response(update)
        return

    if not context.args:
        await update.message.reply_text("Использование: /rate good|bad [task_id]")
        return

    rating = context.args[0].lower()
    if rating not in ("good", "bad"):
        await update.message.reply_text("Оценка должна быть 'good' или 'bad'")
        return

    # Get task_id from args or session
    task_id = None
    if len(context.args) > 1:
        task_id = context.args[1]
    else:
        user_session = session.load_session(user_id)
        task_id = user_session.last_video

    if not task_id:
        await update.message.reply_text("Укажите task_id или сначала просмотрите видео")
        return

    if data_reader.write_feedback(task_id, user_id, "RATE", rating):
        emoji = "👍" if rating == "good" else "👎"
        await update.message.reply_text(f"{emoji} Оценка сохранена для {task_id}")
        data_reader.log_viewer_action(user_id, "RATE", f"{task_id}:{rating}")
    else:
        await update.message.reply_text(f"❌ Генерация {task_id} не найдена")


async def comment(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /comment command."""
    user_id = update.effective_user.id

    if not is_authorized(user_id):
        await unauthorized_response(update)
        return

    if not context.args:
        await update.message.reply_text("Использование: /comment <текст>")
        return

    comment_text = " ".join(context.args)

    # Get task_id from session
    user_session = session.load_session(user_id)
    task_id = user_session.last_video

    if not task_id:
        await update.message.reply_text("Сначала просмотрите видео с помощью /video")
        return

    if data_reader.write_feedback(task_id, user_id, "COMMENT", comment_text):
        await update.message.reply_text(f"💬 Комментарий сохранён для {task_id}")
        data_reader.log_viewer_action(user_id, "COMMENT", f"{task_id}:{comment_text[:50]}")
    else:
        await update.message.reply_text(f"❌ Генерация {task_id} не найдена")


async def shortcut_product(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /p_<source>_<product_id> shortcut command."""
    # Extract source and product_id from command like /p_ozon_123456 or /p_wb_123456
    # Also supports legacy /p_123456 (no source prefix)
    cmd = update.message.text.split()[0]
    rest = cmd[3:]  # Remove /p_
    for pfx in ("wb_", "avito_", "ozon_"):
        if rest.startswith(pfx):
            context.args = [rest[len(pfx):], pfx.rstrip("_")]
            await product(update, context)
            return
    # Legacy: /p_123456 (no source)
    context.args = [rest]
    await product(update, context)


async def shortcut_video(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle /v_<task_id> shortcut command."""
    # Extract task_id from command like /v_20260117_235126_mdpc
    cmd = update.message.text.split()[0]
    task_id = cmd[3:]  # Remove /v_
    context.args = [task_id]
    await video(update, context)


async def text_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle text messages as search queries."""
    user_id = update.effective_user.id

    if not is_authorized(user_id):
        await unauthorized_response(update)
        return

    query = update.message.text.strip()

    # Handle quick references
    user_session = session.load_session(user_id)

    if query.lower() in ("ещё", "еще", "а ещё", "а еще", "more"):
        # Continue previous search
        if user_session.recent_searches:
            last_search = user_session.recent_searches[-1]
            query = f"ещё результаты по запросу: {last_search['query']}"

    elif query.lower() in ("детали", "подробнее", "details"):
        # Show details of last video
        if user_session.last_video:
            context.args = [user_session.last_video]
            await video(update, context)
            return

    await do_search(update, user_id, query)


# =============================================================================
# Admin Commands - Token Management
# =============================================================================

def escape_md(text: str) -> str:
    """Escape Markdown special characters in user-provided text."""
    for ch in ('_', '*', '[', ']', '`'):
        text = text.replace(ch, f'\\{ch}')
    return text


def is_admin(user_id: int) -> bool:
    """Check if user is admin."""
    return user_id in ADMIN_USERS


def get_db_env(context: ContextTypes.DEFAULT_TYPE) -> str:
    """Get current database environment from user context."""
    return context.user_data.get("db_env", DEFAULT_DB_ENV)


def get_current_db_path(context: ContextTypes.DEFAULT_TYPE) -> Path:
    """Get current database path based on user's selected environment."""
    env = get_db_env(context)
    return DB_PATH_DEV if env == "dev" else DB_PATH_PROD


def format_db_status(context: ContextTypes.DEFAULT_TYPE) -> str:
    """Format current DB status indicator."""
    env = get_db_env(context)
    return "🔧 DEV" if env == "dev" else "🚀 PROD"


async def admin_dev(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Switch to DEV database."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    context.user_data["db_env"] = "dev"
    init_db(DB_PATH_DEV)
    await update.message.reply_text(
        f"🔧 Переключено на DEV базу данных\n"
        f"📁 {DB_PATH_DEV}"
    )


async def admin_prod(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Switch to PROD database."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    context.user_data["db_env"] = "prod"
    init_db(DB_PATH_PROD)
    await update.message.reply_text(
        f"🚀 Переключено на PROD базу данных\n"
        f"📁 {DB_PATH_PROD}"
    )


async def admin_users(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """List users with balances."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    page = int(context.args[0]) if context.args else 1
    limit = 15
    offset = (page - 1) * limit

    users = get_all_users(limit=limit, offset=offset, db_path=db_path)
    total = get_users_count(db_path)
    total_pages = (total + limit - 1) // limit

    if not users:
        await update.effective_message.reply_text(f"{status} | Пользователи не найдены")
        return

    text = f"{status} | 👥 *Пользователи* (стр. {page}/{total_pages}, всего: {total})\n\n"

    for u in users:
        blocked = "🚫" if u.get("is_blocked") else ""
        promo = "🎁" if u.get("is_promo_eligible") else ""
        # Build full name: first_name + last_name
        first = u.get("first_name") or ""
        last = u.get("last_name") or ""
        full_name = f"{first} {last}".strip() or u.get("username") or str(u["user_id"])
        spent = u.get("total_spent", 0) or 0
        ref = u.get("ref_source") or ""
        ref_tag = f" `{ref}`" if ref else ""
        gen_tag = f" 🎬{spent}" if spent else ""
        text += f"`{u['user_id']}` {blocked}{promo} {escape_md(full_name)}: *{u['balance']}*{gen_tag}{ref_tag}\n"

    # Navigation buttons
    keyboard = []
    nav_row = []
    if page > 1:
        nav_row.append(InlineKeyboardButton("⬅️ Назад", callback_data=f"admin_users:{page-1}"))
    if page < total_pages:
        nav_row.append(InlineKeyboardButton("Вперёд ➡️", callback_data=f"admin_users:{page+1}"))
    if nav_row:
        keyboard.append(nav_row)

    reply_markup = InlineKeyboardMarkup(keyboard) if keyboard else None
    # Edit message in-place when paginating via callback, reply when from command
    if update.callback_query:
        await update.callback_query.message.edit_text(
            text, parse_mode="Markdown", reply_markup=reply_markup
        )
    else:
        await update.effective_message.reply_text(
            text, parse_mode="Markdown", reply_markup=reply_markup
        )


def _utc_to_moscow(dt_str: str) -> str:
    """Convert UTC datetime string to Moscow time (UTC+3), return formatted string."""
    if not dt_str:
        return "—"
    try:
        # Parse UTC datetime (SQLite stores without timezone)
        dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)
        moscow = dt + timedelta(hours=3)
        return moscow.strftime("%d %b %H:%M")
    except (ValueError, TypeError):
        return dt_str[:16]


def _read_product_name(product_id: str, task_id: str) -> str:
    """Read product name from metadata.json, return truncated name or empty string."""
    for prefix in ("wb", "avito", "ozon"):
        meta_path = Path(f"/mnt/video/{prefix}_{product_id}/{task_id}/metadata.json")
        if meta_path.exists():
            try:
                with open(meta_path) as f:
                    data = json.load(f)
                name = data.get("product_name", "")
                if name:
                    return name[:35] + ("..." if len(name) > 35 else "")
            except (json.JSONDecodeError, OSError):
                pass
    return ""


def _build_generation_history(user_id: int, db_path) -> str:
    """Build generation history section grouped by product."""
    generations = get_user_generations(user_id, limit=50, db_path=db_path)
    if not generations:
        return ""

    # Also collect refund task_ids to mark refunded generations
    from database import get_connection
    with get_connection(db_path) as conn:
        cursor = conn.cursor()
        cursor.execute(
            "SELECT task_id FROM transactions WHERE user_id = ? AND type = 'refund' AND task_id IS NOT NULL",
            (user_id,)
        )
        refund_task_ids = {row["task_id"] for row in cursor.fetchall()}

    # Group by product_id, preserving order of first appearance
    products = OrderedDict()  # product_id -> list of gen dicts
    for gen in generations:
        pid = gen.get("product_id") or "unknown"
        products.setdefault(pid, []).append(gen)

    total_gens = len(generations)
    total_videos = sum(1 for g in generations if g.get("has_video"))
    unique_products = len(products)

    text = f"\n🎬 *Генерации ({total_gens} gen, {total_videos} video, {unique_products} товаров):*\n"

    for pid, gens in products.items():
        # Get product name from the first generation's metadata
        product_name = ""
        for g in gens:
            if g.get("task_id") and g.get("product_id"):
                product_name = _read_product_name(g["product_id"], g["task_id"])
                if product_name:
                    break

        n_gens = len(gens)
        n_videos = sum(1 for g in gens if g.get("has_video"))
        n_refunds = sum(1 for g in gens if g.get("task_id") in refund_task_ids)

        # Build outcome summary
        parts = []
        if n_gens > 0:
            parts.append(f"{n_gens} gen")
        if n_videos > 0:
            parts.append(f"{n_videos} video")
        if n_refunds > 0:
            parts.append(f"{n_refunds} refund")
        outcome = ", ".join(parts)

        raw_name = (product_name or pid).replace('"', "'")
        # Escape chars that break Markdown link syntax [text](url)
        safe_name = raw_name.replace("[", "\\[").replace("]", "\\]").replace("(", "\\(").replace(")", "\\)")
        wb_link = f"https://www.wildberries.ru/catalog/{pid}/detail.aspx"
        text += f"• [{safe_name}]({wb_link}) — {outcome}\n"
        text += f"  📦 /p_{pid}\n"

        # Show each generation with timestamp and viewer link
        for g in gens:
            ts = _utc_to_moscow(g.get("created_at", ""))
            tid = g.get("task_id", "")
            refunded = "↩️" if tid in refund_task_ids else ""
            video_icon = "🎥" if g.get("has_video") else "⏳"
            link = f"/v_{tid}" if tid else ""
            text += f"  {video_icon}{refunded} {ts} {link}\n"

    return text


async def admin_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show detailed user info."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if not context.args:
        await update.message.reply_text("Использование: /user <user_id>")
        return

    target_id = int(context.args[0])
    db_path = get_current_db_path(context)
    status = format_db_status(context)

    info = get_user_info(target_id, db_path)
    if not info:
        await update.message.reply_text(f"{status} | Пользователь {target_id} не найден")
        return

    u = info["user"]
    txns = info["transactions"]

    created_msk = _utc_to_moscow(u.get('created_at', ''))

    username = escape_md(u.get('username') or 'N/A')
    first_name = escape_md(u.get('first_name', '') or '')
    last_name = escape_md(u.get('last_name', '') or '')

    text = f"{status} | 👤 *Пользователь {target_id}*\n\n"
    text += f"Username: @{username}\n"
    text += f"Имя: {first_name} {last_name}\n"
    text += f"Баланс: *{u['balance']}* токенов\n"
    text += f"Куплено всего: {u['total_purchased']}\n"
    text += f"Использовано: {u['total_spent']}\n"
    text += f"Промо: {'✅' if u['is_promo_eligible'] else '❌'}\n"
    text += f"Заблокирован: {'🚫 ДА' if u['is_blocked'] else '✅ Нет'}\n"
    text += f"Источник: {u.get('ref_source') or '—'}\n"
    text += f"Создан: {created_msk} (MSK)\n"

    # Show gift pack info
    all_packs = get_user_packs(target_id, db_path=db_path)
    if all_packs:
        text += f"\n🎁 *Подарки ({len(all_packs)}):*\n"
        status_emoji = {"active": "🟢", "redeemed": "✅", "expired": "⏰"}
        status_label = {"active": "активен", "redeemed": "использован", "expired": "истёк"}
        for p in all_packs:
            s = p["status"]
            emoji = status_emoji.get(s, "•")
            label = status_label.get(s, s)
            line = f"{emoji} {p['tokens']} ген. — {label}"
            if s == "active":
                line += f", до {p['expires_at'][:16]}"
            elif s == "redeemed" and p.get("redeemed_at"):
                line += f" {p['redeemed_at'][:10]}"
            text += f"{line}\n"
    elif u.get('gift_pack_received'):
        text += f"\n🎁 Подарок: получен ранее (удалён)\n"

    # Show generation history
    text += _build_generation_history(target_id, db_path)

    if txns:
        text += f"\n📜 *Последние транзакции:*\n"
        for txn in txns[:10]:
            sign = "+" if txn["amount"] > 0 else ""
            emoji = {
                "purchase": "💳",
                "generation": "🎬",
                "refund": "↩️",
                "admin_grant": "🎁",
                "admin_revoke": "⛔",
                "migration": "📦",
                "giveaway": "🎉",
                "gift_pack_redeem": "🎁",
            }.get(txn["type"], "•")
            desc = escape_md(txn.get("description", "")[:30])
            text += f"{emoji} {sign}{txn['amount']} `{txn['type']}` {desc}\n"

    keyboard = [
        [
            InlineKeyboardButton("🎁 +10", callback_data=f"admin_grant:{target_id}:10"),
            InlineKeyboardButton("🎁 +100", callback_data=f"admin_grant:{target_id}:100"),
        ],
        [
            InlineKeyboardButton("⛔ -10", callback_data=f"admin_revoke:{target_id}:10"),
            InlineKeyboardButton("🚫 Блок" if not u['is_blocked'] else "✅ Разблок",
                               callback_data=f"admin_block:{target_id}" if not u['is_blocked'] else f"admin_unblock:{target_id}"),
        ]
    ]

    try:
        await update.message.reply_text(
            text,
            parse_mode="Markdown",
            reply_markup=InlineKeyboardMarkup(keyboard)
        )
    except telegram.error.BadRequest:
        # Fallback to plain text if Markdown parsing fails
        await update.message.reply_text(
            text,
            reply_markup=InlineKeyboardMarkup(keyboard)
        )


async def admin_grant(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Grant tokens to user."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if len(context.args) < 2:
        await update.message.reply_text("Использование: /grant <user_id> <amount> [reason]")
        return

    target_id = int(context.args[0])
    amount = int(context.args[1])
    reason = " ".join(context.args[2:]) if len(context.args) > 2 else "Admin grant"

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    # Ensure user exists
    get_or_create_user(target_id, db_path=db_path)

    if credit_tokens(target_id, amount, "admin_grant", reason, initiated_by=user_id, db_path=db_path):
        new_balance = get_balance(target_id, db_path)
        await update.message.reply_text(
            f"{status} | ✅ Начислено *{amount}* токенов пользователю {target_id}\n"
            f"Новый баланс: *{new_balance}*\n"
            f"Причина: {reason}",
            parse_mode="Markdown"
        )
    else:
        await update.message.reply_text(f"{status} | ❌ Ошибка начисления")


async def admin_revoke(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Revoke tokens from user."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if len(context.args) < 2:
        await update.message.reply_text("Использование: /revoke <user_id> <amount> [reason]")
        return

    target_id = int(context.args[0])
    amount = int(context.args[1])
    reason = " ".join(context.args[2:]) if len(context.args) > 2 else "Admin revoke"

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    if revoke_tokens(target_id, amount, reason, initiated_by=user_id, db_path=db_path):
        new_balance = get_balance(target_id, db_path)
        await update.message.reply_text(
            f"{status} | ✅ Списано *{amount}* токенов у пользователя {target_id}\n"
            f"Новый баланс: *{new_balance}*\n"
            f"Причина: {reason}",
            parse_mode="Markdown"
        )
    else:
        await update.message.reply_text(
            f"{status} | ❌ Ошибка списания (недостаточно токенов?)"
        )


async def admin_block(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Block user."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if not context.args:
        await update.message.reply_text("Использование: /block <user_id> [reason]")
        return

    target_id = int(context.args[0])
    reason = " ".join(context.args[1:]) if len(context.args) > 1 else "Blocked by admin"

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    if block_user(target_id, reason, initiated_by=user_id, db_path=db_path):
        await update.message.reply_text(
            f"{status} | 🚫 Пользователь {target_id} заблокирован\n"
            f"Причина: {reason}"
        )
    else:
        await update.message.reply_text(f"{status} | ❌ Ошибка блокировки")


async def admin_unblock(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Unblock user."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if not context.args:
        await update.message.reply_text("Использование: /unblock <user_id>")
        return

    target_id = int(context.args[0])
    db_path = get_current_db_path(context)
    status = format_db_status(context)

    if unblock_user(target_id, db_path):
        await update.message.reply_text(f"{status} | ✅ Пользователь {target_id} разблокирован")
    else:
        await update.message.reply_text(f"{status} | ❌ Ошибка разблокировки")


async def admin_dropuser(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Delete user and all their data from the database."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if not context.args:
        await update.message.reply_text("Использование: /dropuser <user_id>")
        return

    target_id = int(context.args[0])
    db_path = get_current_db_path(context)
    status = format_db_status(context)

    target_user = get_user(target_id, db_path)
    if not target_user:
        await update.message.reply_text(f"{status} | ❌ Пользователь {target_id} не найден")
        return

    with get_connection(db_path) as conn:
        c = conn.cursor()
        c.execute("DELETE FROM transactions WHERE user_id = ?", (target_id,))
        txns = c.rowcount
        c.execute("DELETE FROM gift_packs WHERE user_id = ?", (target_id,))
        packs = c.rowcount
        c.execute("DELETE FROM users WHERE user_id = ?", (target_id,))

    await update.message.reply_text(
        f"{status} | 🗑 Пользователь {target_id} удалён\n\n"
        f"Удалено: {txns} транзакций, {packs} подарков"
    )


async def _render_txns_page(update: Update, context: ContextTypes.DEFAULT_TYPE, page: int = 1, txn_type: str = None):
    """Render a page of transactions (shared by command and callback)."""
    db_path = get_current_db_path(context)
    status = format_db_status(context)
    per_page = 20

    total = count_transactions(txn_type=txn_type, db_path=db_path)
    if total == 0:
        text = f"{status} | Транзакции не найдены"
        if update.callback_query:
            await update.callback_query.message.edit_text(text)
        else:
            await update.message.reply_text(text)
        return

    import math
    total_pages = math.ceil(total / per_page)
    page = max(1, min(page, total_pages))
    offset = (page - 1) * per_page

    txns = get_recent_transactions(limit=per_page, txn_type=txn_type, db_path=db_path, offset=offset)

    filter_label = f" `{txn_type}`" if txn_type else ""
    text = f"{status} | 📜 *Транзакции*{filter_label} ({total})\n\n"

    for txn in txns:
        sign = "+" if txn["amount"] > 0 else ""
        emoji = {
            "purchase": "💳",
            "generation": "🎬",
            "refund": "↩️",
            "admin_grant": "🎁",
            "admin_revoke": "⛔",
            "migration": "📦",
            "giveaway": "🎉",
            "gift_pack_redeem": "🎁",
        }.get(txn["type"], "•")
        first = txn.get("first_name") or ""
        last = txn.get("last_name") or ""
        full_name = f"{first} {last}".strip() or txn.get("username") or str(txn["user_id"])
        text += f"{emoji} `{txn['user_id']}` {full_name}: {sign}{txn['amount']} `{txn['type']}`\n"

    # Pagination buttons
    rows = []
    nav_buttons = []
    cb_type = f":{txn_type}" if txn_type else ""
    if page > 1:
        nav_buttons.append(InlineKeyboardButton("⬅️", callback_data=f"txns:{page-1}{cb_type}"))
    nav_buttons.append(InlineKeyboardButton(f"{page}/{total_pages}", callback_data="noop"))
    if page < total_pages:
        nav_buttons.append(InlineKeyboardButton("➡️", callback_data=f"txns:{page+1}{cb_type}"))
    if total_pages > 1:
        rows.append(nav_buttons)

    # Filter buttons
    filters = [
        ("Все", None), ("💳", "purchase"), ("🎬", "generation"),
        ("↩️", "refund"), ("🎁", "giveaway"), ("🎁📦", "gift_pack_redeem"),
    ]
    filter_buttons = []
    for label, ftype in filters:
        active = (ftype == txn_type)
        btn_label = f"[{label}]" if active else label
        cb = f"txns:1:{ftype}" if ftype else "txns:1"
        filter_buttons.append(InlineKeyboardButton(btn_label, callback_data=cb))
    rows.append(filter_buttons)

    keyboard = InlineKeyboardMarkup(rows)

    if update.callback_query:
        await update.callback_query.message.edit_text(text, parse_mode="Markdown", reply_markup=keyboard)
    else:
        await update.message.reply_text(text, parse_mode="Markdown", reply_markup=keyboard)


async def admin_txns(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show recent transactions."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    txn_type = context.args[0] if context.args else None
    await _render_txns_page(update, context, page=1, txn_type=txn_type)


async def handle_txns_page(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle txns pagination callback."""
    query = update.callback_query
    await query.answer()

    # Parse callback: txns:PAGE or txns:PAGE:TYPE
    parts = query.data.split(":")
    page = int(parts[1])
    txn_type = parts[2] if len(parts) > 2 else None

    await _render_txns_page(update, context, page=page, txn_type=txn_type)


async def admin_tokenstats(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show token system statistics."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    stats_data = get_stats(db_path)

    text = f"{status} | 📊 *Статистика токенов*\n\n"
    text += f"👥 Пользователей: {stats_data['total_users']}\n"
    text += f"💰 С балансом: {stats_data['users_with_balance']}\n"
    text += f"🚫 Заблокировано: {stats_data['blocked_users']}\n"
    text += f"🎫 Всего токенов: {stats_data['total_balance']}\n"
    text += f"💵 Выручка: {stats_data['total_revenue_rub']:.2f} ₽\n"
    text += f"\n🎁 Промо:\n"
    text += f"  Пользователей с промо: {stats_data['promo_users']}\n"
    text += f"  Осталось слотов: {stats_data['promo_remaining']}\n"

    text += f"\n📜 Транзакции по типам:\n"
    for txn_type, data in stats_data.get("transactions", {}).items():
        text += f"  {txn_type}: {data['count']} ({data['total']} токенов)\n"

    await update.message.reply_text(text, parse_mode="Markdown")


async def admin_promo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show or set promo configuration."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    if context.args:
        # Set promo remaining
        count = int(context.args[0])
        set_promo_remaining(count, db_path)
        await update.message.reply_text(
            f"{status} | ✅ Установлено промо-слотов: *{count}*",
            parse_mode="Markdown"
        )
    else:
        # Show current promo status
        remaining = get_promo_remaining(db_path)
        await update.message.reply_text(
            f"{status} | 🎁 Промо-слотов осталось: *{remaining}*",
            parse_mode="Markdown"
        )


async def admin_giveaway_start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Start a token giveaway campaign."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if not context.args:
        await update.message.reply_text(
            "Использование: /giveaway_start <users> [tokens]\n"
            "Пример: /giveaway_start 100 - первые 100 пользователей получат 3 токена\n"
            "Пример: /giveaway_start 50 5 - первые 50 пользователей получат 5 токенов"
        )
        return

    try:
        users = int(context.args[0])
        tokens = int(context.args[1]) if len(context.args) > 1 else 3

        if users <= 0 or tokens <= 0:
            await update.message.reply_text("❌ Количество пользователей и токенов должно быть больше 0")
            return

        db_path = get_current_db_path(context)
        status = format_db_status(context)

        # Import giveaway functions
        from database import start_giveaway

        start_giveaway(tokens, users, user_id, db_path)

        await update.message.reply_text(
            f"{status} | ✅ *Раздача токенов запущена!*\n\n"
            f"👥 Пользователей: {users}\n"
            f"🎁 Токенов каждому: {tokens}\n"
            f"📊 Всего токенов: {users * tokens}",
            parse_mode="Markdown"
        )

    except ValueError:
        await update.message.reply_text("❌ Неверный формат. Используйте числа.")


async def admin_giveaway_stop(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Stop the active giveaway campaign."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    # Import giveaway functions
    from database import stop_giveaway, is_giveaway_active

    if not is_giveaway_active(db_path):
        await update.message.reply_text(f"{status} | ℹ️ Раздача не активна")
        return

    stats = stop_giveaway(db_path)

    await update.message.reply_text(
        f"{status} | ✅ *Раздача токенов остановлена*\n\n"
        f"📊 Статистика:\n"
        f"👥 Пользователей получили: {stats['users_granted']}\n"
        f"🎁 Токенов роздано: {stats['tokens_granted']}\n"
        f"📉 Осталось слотов: {stats['remaining_users']}",
        parse_mode="Markdown"
    )


# =============================================================================
# Gift Pack Admin Commands
# =============================================================================

async def admin_giftcamp(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Manage gift pack campaigns."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    if not context.args:
        await update.message.reply_text(
            f"{status} | Использование:\n"
            "/giftcamp list — показать кампании\n"
            "/giftcamp new\\_user <tokens> <days> [slots] — для новых пользователей\n"
            "/giftcamp deeplink <source1,source2,...> <tokens> <days> [slots] — по deeplink\n"
            "/giftcamp stop <id> — остановить кампанию",
            parse_mode="Markdown"
        )
        return

    subcmd = context.args[0]

    if subcmd == "list":
        campaigns = get_all_campaigns(include_inactive=True, db_path=db_path)
        if not campaigns:
            await update.message.reply_text(f"{status} | Кампаний нет")
            return

        bot_username = get_main_bot_username(context)
        text = f"{status} | 📋 <b>Кампании подарков</b>\n\n"
        for c in campaigns:
            active_mark = "✅" if c["is_active"] else "❌"
            slots = "∞" if c["remaining_slots"] == -1 else str(c["remaining_slots"])
            ref = f" <code>{c['ref_source']}</code>" if c.get("ref_source") else ""
            text += (
                f"{active_mark} <b>#{c['id']}</b> <code>{c['campaign_type']}</code>{ref}\n"
                f"  {c['tokens']} ген. × {c['expiration_days']} дней, слотов: {slots}\n"
            )
            if c.get("ref_source") and c["campaign_type"] == "deeplink":
                link = f"https://t.me/{bot_username}?start={c['ref_source']}"
                text += f"  {link}\n"
        await update.message.reply_text(text, parse_mode="HTML")

    elif subcmd == "new_user":
        if len(context.args) < 3:
            await update.message.reply_text("Использование: /giftcamp new_user <tokens> <days> [slots]")
            return
        try:
            tokens = int(context.args[1])
            days = int(context.args[2])
            slots = int(context.args[3]) if len(context.args) > 3 else -1
            camp_id = create_gift_pack_campaign(
                campaign_type="new_user",
                tokens=tokens,
                expiration_days=days,
                remaining_slots=slots,
                description=f"New user campaign: {tokens} tokens for {days} days",
                created_by=user_id,
                db_path=db_path
            )
            slots_text = "безлимит" if slots == -1 else str(slots)
            await update.message.reply_text(
                f"{status} | ✅ *Кампания #{camp_id} создана*\n\n"
                f"Тип: new\\_user\n"
                f"Токенов: {tokens}\n"
                f"Дней: {days}\n"
                f"Слотов: {slots_text}",
                parse_mode="Markdown"
            )
        except ValueError:
            await update.message.reply_text("❌ Неверный формат чисел")

    elif subcmd == "deeplink":
        if len(context.args) < 4:
            await update.message.reply_text(
                "Использование: /giftcamp deeplink <source1,source2,...> <tokens> <days> [slots]"
            )
            return
        try:
            sources = [s.strip() for s in context.args[1].split(",") if s.strip()]
            tokens = int(context.args[2])
            days = int(context.args[3])
            slots = int(context.args[4]) if len(context.args) > 4 else -1
            bot_username = get_main_bot_username(context)
            slots_text = "безлимит" if slots == -1 else str(slots)
            created = []
            for ref_source in sources:
                camp_id = create_gift_pack_campaign(
                    campaign_type="deeplink",
                    tokens=tokens,
                    expiration_days=days,
                    ref_source=ref_source,
                    remaining_slots=slots,
                    description=f"Deeplink campaign for {ref_source}: {tokens} tokens for {days} days",
                    created_by=user_id,
                    db_path=db_path
                )
                created.append((camp_id, ref_source))

            text = f"{status} | ✅ <b>Создано кампаний: {len(created)}</b>\n"
            text += f"Токенов: {tokens}, дней: {days}, слотов: {slots_text}\n\n"
            for camp_id, ref_source in created:
                link = f"https://t.me/{bot_username}?start={ref_source}"
                text += f"<b>#{camp_id}</b> <code>{ref_source}</code>\n{link}\n\n"
            await update.message.reply_text(text, parse_mode="HTML")
        except ValueError:
            await update.message.reply_text("❌ Неверный формат чисел")

    elif subcmd == "stop":
        if len(context.args) < 2:
            await update.message.reply_text("Использование: /giftcamp stop <id>")
            return
        try:
            camp_id = int(context.args[1])
            if deactivate_campaign(camp_id, db_path):
                await update.message.reply_text(f"{status} | ✅ Кампания #{camp_id} остановлена")
            else:
                await update.message.reply_text(f"{status} | ❌ Кампания #{camp_id} не найдена")
        except ValueError:
            await update.message.reply_text("❌ Неверный ID кампании")

    else:
        await update.message.reply_text("❌ Неизвестная подкоманда. Используйте: list, new_user, deeplink, stop")


async def admin_giftpack(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Send a gift pack to a specific user (admin override)."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if len(context.args) < 2:
        await update.message.reply_text("Использование: /giftpack <user_id> <tokens> [days]")
        return

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    try:
        target_id = int(context.args[0])
        tokens = int(context.args[1])
        days = int(context.args[2]) if len(context.args) > 2 else 7

        # Ensure user exists
        target_user = get_user(target_id, db_path)
        if not target_user:
            await update.message.reply_text(f"{status} | ❌ Пользователь {target_id} не найден")
            return

        # Create pack (admin override — bypasses one-time check)
        pack_id = create_gift_pack(
            user_id=target_id,
            tokens=tokens,
            expiration_days=days,
            campaign_id=f"admin_{user_id}",
            description=f"Admin gift from {user_id}",
            created_by=user_id,
            db_path=db_path
        )

        # Mark user as having received a gift pack
        set_gift_pack_received(target_id, db_path)

        await update.message.reply_text(
            f"{status} | ✅ *Подарок отправлен*\n\n"
            f"Пользователь: {target_id}\n"
            f"Токенов: {tokens}\n"
            f"Дней: {days}\n"
            f"Pack ID: #{pack_id}",
            parse_mode="Markdown"
        )

    except ValueError:
        await update.message.reply_text("❌ Неверный формат. Используйте числа.")


async def admin_giftpacks(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """View gift packs — for specific user or aggregate stats."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    if context.args:
        # Show packs for specific user
        try:
            target_id = int(context.args[0])
            packs = get_user_packs(target_id, db_path=db_path)
            if not packs:
                await update.message.reply_text(f"{status} | У пользователя {target_id} нет подарков")
                return

            text = f"{status} | 🎁 *Подарки пользователя {target_id}*\n\n"
            for p in packs:
                status_emoji = {"active": "✅", "redeemed": "💰", "expired": "⏰"}.get(p["status"], "•")
                text += (
                    f"{status_emoji} #{p['id']}: {p['tokens']} ген. `{p['status']}`\n"
                    f"  До: {p['expires_at'][:16]}"
                )
                if p.get("redeemed_at"):
                    text += f" | Активирован: {p['redeemed_at'][:16]}"
                text += "\n"

            await update.message.reply_text(text, parse_mode="Markdown")
        except ValueError:
            await update.message.reply_text("❌ Неверный user_id")
    else:
        # Show aggregate stats
        stats = get_gift_pack_stats(db_path)
        text = f"{status} | 📊 *Статистика подарков*\n\n"
        text += f"Всего: {stats['total']}\n\n"
        for s, data in stats["by_status"].items():
            emoji = {"active": "✅", "redeemed": "💰", "expired": "⏰"}.get(s, "•")
            text += f"{emoji} {s}: {data['count']} ({data['tokens']} токенов)\n"

        if stats["by_campaign"]:
            text += "\n*По кампаниям:*\n"
            for cid, statuses in stats["by_campaign"].items():
                total_count = sum(s["count"] for s in statuses.values())
                text += f"  `{cid}`: {total_count} подарков\n"

        await update.message.reply_text(text, parse_mode="Markdown")


async def admin_giftbulk(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Bulk send gift packs to inactive users."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if len(context.args) < 2:
        await update.message.reply_text(
            "Использование: /giftbulk <tokens> <days> [message]\n"
            "Отправит подарки всем неактивным пользователям (0 покупок, 0 генераций, нет подарков)"
        )
        return

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    try:
        tokens = int(context.args[0])
        days = int(context.args[1])
    except ValueError:
        await update.message.reply_text("❌ Неверный формат чисел")
        return

    inactive = get_inactive_users(db_path)

    if not inactive:
        await update.message.reply_text(f"{status} | Неактивных пользователей не найдено")
        return

    # Store bulk send params for confirmation
    context.user_data["pending_giftbulk"] = {
        "tokens": tokens,
        "days": days,
        "user_count": len(inactive),
        "users": [u["user_id"] for u in inactive],
    }

    keyboard = InlineKeyboardMarkup([
        [
            InlineKeyboardButton("✅ Отправить", callback_data="confirm_giftbulk"),
            InlineKeyboardButton("❌ Отмена", callback_data="cancel_giftbulk"),
        ]
    ])

    await update.message.reply_text(
        f"{status} | 📦 *Массовая отправка подарков*\n\n"
        f"Неактивных пользователей: *{len(inactive)}*\n"
        f"Токенов каждому: {tokens}\n"
        f"Срок: {days} дней\n\n"
        f"Подтвердите отправку:",
        parse_mode="Markdown",
        reply_markup=keyboard
    )


async def handle_confirm_giftbulk(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle confirmation of bulk gift pack send."""
    query = update.callback_query
    user_id = query.from_user.id
    if not is_admin(user_id):
        await query.answer("⛔ Только для администраторов")
        return

    await query.answer()

    params = context.user_data.pop("pending_giftbulk", None)
    if not params:
        await query.edit_message_text("❌ Данные истекли. Повторите /giftbulk")
        return

    db_path = get_current_db_path(context)
    from datetime import datetime
    campaign_id = f"gp_{datetime.now().strftime('%Y%m%d_%H%M%S')}"

    sent = 0
    failed = 0

    await query.edit_message_text(f"⏳ Отправляю подарки {params['user_count']} пользователям...")

    for target_id in params["users"]:
        try:
            pack_id = create_gift_pack(
                user_id=target_id,
                tokens=params["tokens"],
                expiration_days=params["days"],
                campaign_id=campaign_id,
                description=f"Bulk gift: {params['tokens']} tokens for {params['days']} days",
                created_by=user_id,
                db_path=db_path
            )
            set_gift_pack_received(target_id, db_path)
            sent += 1
        except Exception as e:
            logging.warning(f"Failed to create gift pack for {target_id}: {e}")
            failed += 1

        await asyncio.sleep(0.02)  # Rate limit

    await query.edit_message_text(
        f"✅ *Массовая отправка завершена*\n\n"
        f"Campaign: `{campaign_id}`\n"
        f"Отправлено: {sent}\n"
        f"Ошибок: {failed}",
        parse_mode="Markdown"
    )


async def handle_cancel_giftbulk(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle cancellation of bulk gift pack send."""
    query = update.callback_query
    await query.answer()
    context.user_data.pop("pending_giftbulk", None)
    await query.edit_message_text("❌ Массовая отправка отменена")


async def admin_search(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Search users by username or ID."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if not context.args:
        await update.message.reply_text("Использование: /usearch <query>")
        return

    query = " ".join(context.args)
    db_path = get_current_db_path(context)
    status = format_db_status(context)

    users = search_users(query, limit=20, db_path=db_path)

    if not users:
        await update.message.reply_text(f"{status} | Пользователи не найдены: {query}")
        return

    text = f"{status} | 🔍 *Поиск: {query}*\n\n"
    for u in users:
        blocked = "🚫" if u.get("is_blocked") else ""
        promo = "🎁" if u.get("is_promo_eligible") else ""
        # Build full name: first_name + last_name
        first = u.get("first_name") or ""
        last = u.get("last_name") or ""
        full_name = f"{first} {last}".strip() or u.get("username") or str(u["user_id"])
        spent = u.get("total_spent", 0) or 0
        ref = u.get("ref_source") or ""
        ref_tag = f" `{ref}`" if ref else ""
        gen_tag = f" 🎬{spent}" if spent else ""
        text += f"`{u['user_id']}` {blocked}{promo} {escape_md(full_name)}: *{u['balance']}*{gen_tag}{ref_tag}\n"

    await update.message.reply_text(text, parse_mode="Markdown")


async def admin_refstats(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Show aggregate referral source statistics."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    rows = get_ref_stats(db_path)

    if not rows:
        await update.message.reply_text(f"{status} | Пользователи не найдены")
        return

    text = f"{status} | 📊 *Источники пользователей*\n\n"

    for row in rows:
        source = row["ref_source"]
        count = row["count"]
        if source:
            text += f"`{source}`: {count}\n"
        else:
            text += f"(без источника): {count}\n"

    await update.message.reply_text(text, parse_mode="Markdown")


# =============================================================================
# Survey Admin Commands
# =============================================================================

async def admin_survey(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Manage user feedback surveys."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    if not context.args:
        await update.message.reply_text(
            f"{status} | Использование:\n"
            "/survey list — активные опросы\n"
            "/survey create [title] — создать опрос\n"
            "/survey send <id> [count] — отправить неактивным\n"
            "/survey test <id> <user\\_id> — отправить конкретному\n"
            "/survey stats <id> — результаты\n"
            "/survey stop <id> — деактивировать\n"
            "/survey export <id> — все ответы",
            parse_mode="Markdown",
        )
        return

    subcmd = context.args[0]

    # --- LIST ---
    if subcmd == "list":
        surveys = get_active_surveys(db_path)
        if not surveys:
            await update.message.reply_text(f"{status} | Нет активных опросов")
            return
        text = f"{status} | 📋 *Активные опросы*\n\n"
        for s in surveys:
            stats = get_survey_stats(s["id"], db_path)
            text += (
                f"*#{s['id']}* {escape_md(s['title'])}\n"
                f"  Отправлено: {stats['total_sent']} | "
                f"Начато: {stats['started']} | "
                f"Завершено: {stats['completed']}\n\n"
            )
        await update.message.reply_text(text, parse_mode="Markdown")

    # --- CREATE ---
    elif subcmd == "create":
        title = " ".join(context.args[1:]) if len(context.args) > 1 else "User Feedback Survey"
        survey_id = create_survey(
            title=title,
            reward_tokens=3,
            reward_days=7,
            created_by=user_id,
            db_path=db_path,
        )
        await update.message.reply_text(
            f"{status} | ✅ Опрос #{survey_id} создан: {title}\n"
            f"Награда: 3 генерации на 7 дней\n\n"
            f"Отправить: /survey send {survey_id} 50"
        )

    # --- SEND ---
    elif subcmd == "send":
        if len(context.args) < 2:
            await update.message.reply_text(f"{status} | /survey send <id> [count]")
            return
        try:
            survey_id = int(context.args[1])
        except ValueError:
            await update.message.reply_text(f"{status} | ❌ id должен быть числом")
            return

        survey = get_survey(survey_id, db_path)
        if not survey or not survey["is_active"]:
            await update.message.reply_text(f"{status} | ❌ Опрос #{survey_id} не найден или неактивен")
            return

        count = 50
        if len(context.args) > 2:
            try:
                count = int(context.args[2])
            except ValueError:
                pass

        targets = get_survey_target_users(limit=count, db_path=db_path)
        if not targets:
            await update.message.reply_text(f"{status} | Нет подходящих пользователей для отправки")
            return

        main_token = get_main_bot_token(context)
        if not main_token:
            env = get_db_env(context)
            await update.message.reply_text(f"{status} | ❌ MAIN_BOT_TOKEN_{env.upper()} не настроен")
            return

        progress = await update.message.reply_text(
            f"{status} | 📤 Отправка опроса #{survey_id} ({len(targets)} пользователей)..."
        )

        bot = telegram.Bot(token=main_token)
        sent = 0
        failed = 0
        for target in targets:
            ok = await send_survey_invite(bot, target["user_id"], survey_id, db_path)
            if ok:
                sent += 1
            else:
                failed += 1
            # Rate limit: 1 message per 50ms (Telegram allows ~30/sec)
            import asyncio
            await asyncio.sleep(0.05)

        await progress.edit_text(
            f"{status} | ✅ Опрос #{survey_id} отправлен\n"
            f"Отправлено: {sent}\n"
            f"Ошибки: {failed}"
        )

    # --- TEST ---
    elif subcmd == "test":
        if len(context.args) < 3:
            await update.message.reply_text(f"{status} | /survey test <id> <user\\_id>", parse_mode="Markdown")
            return
        try:
            survey_id = int(context.args[1])
            target_id = int(context.args[2])
        except ValueError:
            await update.message.reply_text(f"{status} | ❌ id и user_id должны быть числами")
            return

        survey = get_survey(survey_id, db_path)
        if not survey:
            await update.message.reply_text(f"{status} | ❌ Опрос #{survey_id} не найден")
            return

        main_token = get_main_bot_token(context)
        if not main_token:
            env = get_db_env(context)
            await update.message.reply_text(f"{status} | ❌ MAIN_BOT_TOKEN_{env.upper()} не настроен")
            return

        bot = telegram.Bot(token=main_token)
        ok = await send_survey_invite(bot, target_id, survey_id, db_path)
        if ok:
            await update.message.reply_text(f"{status} | ✅ Опрос отправлен пользователю {target_id}")
        else:
            await update.message.reply_text(
                f"{status} | ❌ Не удалось отправить (бот заблокирован? уже приглашён?)"
            )

    # --- STATS ---
    elif subcmd == "stats":
        if len(context.args) < 2:
            await update.message.reply_text(f"{status} | /survey stats <id>")
            return
        try:
            survey_id = int(context.args[1])
        except ValueError:
            await update.message.reply_text(f"{status} | ❌ id должен быть числом")
            return

        survey = get_survey(survey_id, db_path)
        if not survey:
            await update.message.reply_text(f"{status} | ❌ Опрос #{survey_id} не найден")
            return

        stats = get_survey_stats(survey_id, db_path)
        questions = json.loads(survey["questions_json"])
        q_map = {q["key"]: q for q in questions}

        text = (
            f"{status} | 📊 *Опрос #{survey_id}: {escape_md(survey['title'])}*\n\n"
            f"Отправлено: {stats['total_sent']}\n"
            f"Начато: {stats['started']}\n"
            f"Завершено: {stats['completed']} ({stats['completion_rate']}%)\n"
        )

        if stats["nps_score"] is not None:
            text += f"NPS: *{stats['nps_score']}* (n={stats['nps_total']})\n"

        text += "\n"

        for q_key, dist in stats["questions"].items():
            q_def = q_map.get(q_key, {})
            q_text = q_def.get("text", q_key)[:50]
            text += f"*{escape_md(q_text)}*\n"

            if q_key == "improvement":
                # Show free text answers
                for answer, count in sorted(dist.items(), key=lambda x: -x[1]):
                    if answer == "(пропущено)":
                        text += f"  _(пропущено): {count}_\n"
                    else:
                        text += f"  • {escape_md(answer[:80])}\n"
            elif q_key in ("video_usage", "stop_reason", "fair_price", "voice_messages"):
                options = q_def.get("options", [])
                for answer, count in sorted(dist.items(), key=lambda x: -x[1]):
                    # Multi-select answers stored as pipe-separated indices
                    if q_key == "stop_reason" and "|" in answer:
                        indices = answer.split("|")
                        labels = []
                        for idx in indices:
                            try:
                                labels.append(options[int(idx)][:30])
                            except (ValueError, IndexError):
                                labels.append(idx)
                        label = " + ".join(labels)
                    else:
                        try:
                            label = options[int(answer)]
                        except (ValueError, IndexError):
                            label = answer
                    text += f"  {escape_md(label[:50])}: {count}\n"
            else:
                for answer, count in sorted(dist.items()):
                    text += f"  {escape_md(str(answer))}: {count}\n"

            text += "\n"

        # Truncate if too long for Telegram
        if len(text) > 4000:
            text = text[:3990] + "\n..."

        await update.message.reply_text(text, parse_mode="Markdown")

    # --- STOP ---
    elif subcmd == "stop":
        if len(context.args) < 2:
            await update.message.reply_text(f"{status} | /survey stop <id>")
            return
        try:
            survey_id = int(context.args[1])
        except ValueError:
            await update.message.reply_text(f"{status} | ❌ id должен быть числом")
            return

        if deactivate_survey(survey_id, db_path):
            await update.message.reply_text(f"{status} | ✅ Опрос #{survey_id} деактивирован")
        else:
            await update.message.reply_text(f"{status} | ❌ Опрос #{survey_id} не найден")

    # --- EXPORT ---
    elif subcmd == "export":
        if len(context.args) < 2:
            await update.message.reply_text(f"{status} | /survey export <id>")
            return
        try:
            survey_id = int(context.args[1])
        except ValueError:
            await update.message.reply_text(f"{status} | ❌ id должен быть числом")
            return

        survey = get_survey(survey_id, db_path)
        if not survey:
            await update.message.reply_text(f"{status} | ❌ Опрос #{survey_id} не найден")
            return

        questions = json.loads(survey["questions_json"])
        q_map = {q["key"]: q for q in questions}
        responses = get_all_responses_export(survey_id, db_path)

        if not responses:
            await update.message.reply_text(f"{status} | Нет ответов для экспорта")
            return

        text = f"{status} | 📤 *Экспорт опроса #{survey_id}* ({len(responses)} записей)\n\n"

        for r in responses:
            if not r["answers"]:
                continue
            name = r.get("first_name") or r.get("username") or str(r["user_id"])
            text += f"*{escape_md(name)}* (`{r['user_id']}`) — {r['status']}\n"
            for q_key, answer in r["answers"].items():
                q_def = q_map.get(q_key, {})
                options = q_def.get("options", [])
                # Resolve option index to text
                if q_def.get("type") in ("single",) and answer.isdigit():
                    try:
                        answer = options[int(answer)]
                    except (ValueError, IndexError):
                        pass
                elif q_def.get("type") == "multi" and "|" in answer:
                    indices = answer.split("|")
                    labels = []
                    for idx in indices:
                        try:
                            labels.append(options[int(idx)])
                        except (ValueError, IndexError):
                            labels.append(idx)
                    answer = "; ".join(labels)
                label = q_def.get("text", q_key)[:30]
                text += f"  {escape_md(label)}: {escape_md(str(answer)[:80])}\n"
            text += "\n"

        # Split into multiple messages if needed
        if len(text) > 4000:
            parts = [text[i:i+4000] for i in range(0, len(text), 4000)]
            for part in parts:
                await update.message.reply_text(part, parse_mode="Markdown")
        else:
            await update.message.reply_text(text, parse_mode="Markdown")

    else:
        await update.message.reply_text(f"{status} | ❌ Неизвестная подкоманда: {subcmd}")


# =============================================================================
# Gift Video Delivery
# =============================================================================

def get_main_bot_token(context: ContextTypes.DEFAULT_TYPE) -> str:
    """Get main bot token based on current db_env."""
    env = get_db_env(context)
    return MAIN_BOT_TOKEN_DEV if env == "dev" else MAIN_BOT_TOKEN_PROD


def get_main_bot_username(context: ContextTypes.DEFAULT_TYPE) -> str:
    """Get main bot username based on current db_env."""
    env = get_db_env(context)
    return MAIN_BOT_USERNAME_DEV if env == "dev" else MAIN_BOT_USERNAME_PROD


async def admin_gift(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Send a gift video to a user via the main bot.

    Usage: /gift <user_id> <task_id> [message]
    """
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if len(context.args) < 2:
        await update.message.reply_text(
            "Использование: /gift <user_id> <task_id> [message]\n\n"
            "Отправляет видео пользователю через основного бота с кнопками Поправить/Другой вариант.\n"
            "Пример: /gift 100000001 20260205_142848_w5ij\n"
            "Пример: /gift 100000001 20260205_142848_w5ij Извините за ошибку!"
        )
        return

    try:
        target_user_id = int(context.args[0])
    except ValueError:
        await update.message.reply_text("❌ user_id должен быть числом")
        return
    task_id = context.args[1]

    custom_message = " ".join(context.args[2:]) if len(context.args) > 2 else None

    db_path = get_current_db_path(context)
    status = format_db_status(context)

    # Validate main bot token
    main_token = get_main_bot_token(context)
    if not main_token:
        env = get_db_env(context)
        await update.message.reply_text(
            f"{status} | ❌ MAIN_BOT_TOKEN_{env.upper()} не настроен в .env"
        )
        return

    # Validate user exists in database
    target_user = get_user(target_user_id, db_path=db_path)
    if not target_user:
        await update.message.reply_text(
            f"{status} | ❌ Пользователь {target_user_id} не найден в базе"
        )
        return

    # Find task directory
    task_dir = None
    product_id = None
    source = "wb"

    for product_dir in NFS_BASE.iterdir():
        if not product_dir.is_dir():
            continue
        # Support wb_, avito_, ozon_ prefixes
        prefix = None
        for pfx in ("wb_", "avito_", "ozon_"):
            if product_dir.name.startswith(pfx):
                prefix = pfx.rstrip("_")
                break
        if not prefix:
            continue

        candidate = product_dir / task_id
        if candidate.is_dir():
            task_dir = candidate
            product_id = product_dir.name[len(prefix) + 1:]
            source = prefix
            break

    if not task_dir:
        await update.message.reply_text(f"{status} | ❌ Задача {task_id} не найдена")
        return

    # Check video exists
    video_path = task_dir / "videos" / "video.mp4"
    if not video_path.exists():
        await update.message.reply_text(
            f"{status} | ❌ Видео не найдено: {task_id}/videos/video.mp4"
        )
        return

    # Load catchy_name from metadata.json
    catchy_name = "Видео"
    metadata_path = task_dir / "metadata.json"
    if metadata_path.exists():
        try:
            with open(metadata_path, "r", encoding="utf-8") as f:
                metadata = json.load(f)
            story_vision = metadata.get("story_vision", {})
            catchy_name = story_vision.get("catchy_name", "Видео")
        except Exception:
            pass

    # Build caption
    if custom_message:
        caption = f"🎁 {custom_message}\n\n"
    else:
        caption = (
            "🎁 Подарочная генерация\n"
            "Это бесплатная генерация в качестве компенсации за возникшие неудобства.\n\n"
        )
    caption += f"🎬 «{catchy_name}»"

    # Build keyboard with rate buttons (same format as main bot)
    keyboard = [
        [
            InlineKeyboardButton(
                "✏️ Поправить", callback_data=f"rate:{product_id}:{task_id}:good"
            ),
            InlineKeyboardButton(
                "🔄 Другой вариант", callback_data=f"rate:{product_id}:{task_id}:bad"
            ),
        ],
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)

    # Send video via main bot
    await update.message.reply_text(f"{status} | 📤 Отправляю видео...")

    try:
        if TG_PROXY:
            import httpx
            url = f"https://api.telegram.org/bot{main_token}/sendVideo"
            data = {"chat_id": str(target_user_id), "caption": caption,
                    "reply_markup": json.dumps(reply_markup.to_dict())}
            with open(video_path, "rb") as f:
                files = {"video": (Path(video_path).name, f, "video/mp4")}
                async with httpx.AsyncClient(proxy=TG_PROXY, timeout=600) as client:
                    resp = await client.post(url, data=data, files=files)
            if resp.status_code != 200:
                raise Exception(f"Proxy send failed: HTTP {resp.status_code}")
        else:
            main_bot = telegram.Bot(token=main_token)
            with open(video_path, "rb") as f:
                await main_bot.send_video(
                    chat_id=target_user_id,
                    video=f,
                    caption=caption,
                    reply_markup=reply_markup,
                    read_timeout=300,
                    write_timeout=300,
                )
    except Exception as e:
        await update.message.reply_text(
            f"{status} | ❌ Ошибка отправки: {e}"
        )
        return

    # Record gift transaction in database (so it appears in /history)
    try:
        from database import get_connection
        db_path = get_current_db_path(context)
        with get_connection(db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT balance FROM users WHERE user_id = ?", (target_user_id,))
            row = cursor.fetchone()
            balance = row["balance"] if row else 0
            cursor.execute("""
                INSERT INTO transactions (user_id, amount, balance_before, balance_after,
                    type, description, task_id, product_id, initiated_by)
                VALUES (?, 0, ?, ?, 'gift', ?, ?, ?, ?)
            """, (target_user_id, balance, balance,
                  custom_message or "Gift video from admin",
                  task_id, product_id, user_id))
            conn.commit()
    except Exception as e:
        logger.warning(f"Failed to record gift transaction: {e}")

    # Mark as delivered
    try:
        (task_dir / ".delivered").write_text(datetime.now().isoformat())
    except Exception:
        pass

    # Log GIFT action to activity.log
    try:
        activity_log = NFS_BASE / f"{source}_{product_id}" / "activity.log"
        activity_log.parent.mkdir(parents=True, exist_ok=True)
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        line = f"{timestamp} | user:{target_user_id} | GIFT | {task_id} | by admin:{user_id}"
        if custom_message:
            line += f" | {custom_message}"
        line += "\n"
        with open(activity_log, "a") as f:
            f.write(line)
    except Exception as e:
        logger.warning(f"Failed to log GIFT action: {e}")

    # Confirm to admin
    target_name = target_user.get("first_name", "") or target_user.get("username", str(target_user_id))
    await update.message.reply_text(
        f"{status} | ✅ Видео отправлено!\n\n"
        f"👤 Получатель: {target_name} ({target_user_id})\n"
        f"🎬 Задача: {task_id}\n"
        f"📦 Товар: {product_id}\n"
        f"💬 Сообщение: {custom_message or '(стандартное)'}"
    )


# =============================================================================
# Showcase Admin Commands
# =============================================================================

async def admin_showcase(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """List showcase videos with pagination."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    page = int(context.args[0]) if context.args else 1
    limit = 15
    offset = (page - 1) * limit

    videos = get_showcase_videos(active_only=False, exclude_artifacts=False, limit=limit, offset=offset)
    total = get_showcase_videos_count(active_only=False, exclude_artifacts=False)
    total_pages = (total + limit - 1) // limit if total > 0 else 1

    if not videos:
        await update.message.reply_text("📺 Портфолио пусто")
        return

    text = f"📺 *Портфолио* (стр. {page}/{total_pages}, всего: {total})\n\n"

    for v in videos:
        status_icons = ""
        if not v.get("is_active"):
            status_icons += "🚫"
        if v.get("has_artifacts"):
            status_icons += "⚠️"

        rating = v.get("rating", 0)
        stars = "⭐" * rating if rating > 0 else "—"

        # Get product name from generation data
        product_name = ""
        gen = data_reader.get_generation(v['task_id'])
        if gen and gen.product_name:
            # Truncate long names and escape markdown
            name = gen.product_name[:40]
            if len(gen.product_name) > 40:
                name += "..."
            product_name = f" {name}"

        # Escape underscores in task_id for Markdown
        task_id_escaped = v['task_id'].replace('_', '\\_')
        text += f"`{v['id']}` {status_icons} {stars}{product_name} /v\\_{task_id_escaped}\n"

    # Navigation buttons
    keyboard = []
    nav_row = []
    if page > 1:
        nav_row.append(InlineKeyboardButton("⬅️", callback_data=f"showcase_page:{page-1}"))
    nav_row.append(InlineKeyboardButton(f"{page}/{total_pages}", callback_data="noop"))
    if page < total_pages:
        nav_row.append(InlineKeyboardButton("➡️", callback_data=f"showcase_page:{page+1}"))
    if nav_row:
        keyboard.append(nav_row)

    await update.message.reply_text(
        text,
        parse_mode="Markdown",
        reply_markup=InlineKeyboardMarkup(keyboard) if keyboard else None
    )


async def admin_showcase_add(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Add video to showcase by task_id."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if not context.args:
        await update.message.reply_text("Использование: /showcase_add <task_id> [rating]")
        return

    task_id = context.args[0]
    rating = int(context.args[1]) if len(context.args) > 1 else 0

    # Get generation info
    gen = data_reader.get_generation(task_id)
    if not gen:
        await update.message.reply_text(f"❌ Генерация {task_id} не найдена")
        return

    if not gen.has_video or not gen.video_path:
        await update.message.reply_text(f"❌ Видео для {task_id} не найдено")
        return

    # Check if already in showcase
    existing = get_showcase_video_by_task(task_id)
    if existing:
        await update.message.reply_text(f"⚠️ Уже в портфолио (ID: {existing['id']})")
        return

    # Add to showcase
    video_id = add_showcase_video(
        video_path=gen.video_path,
        task_id=task_id,
        product_id=gen.product_id,
        rating=rating,
        has_artifacts=False,
        added_by=user_id
    )

    if video_id:
        await update.message.reply_text(
            f"✅ Добавлено в портфолио\n"
            f"ID: {video_id}\n"
            f"Task: {task_id}\n"
            f"Рейтинг: {'⭐' * rating if rating > 0 else '—'}"
        )
    else:
        await update.message.reply_text("❌ Ошибка добавления")


async def admin_showcase_rm(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Remove video from showcase by ID."""
    user_id = update.effective_user.id
    if not is_admin(user_id):
        await update.message.reply_text("⛔ Только для администраторов")
        return

    if not context.args:
        await update.message.reply_text("Использование: /showcase_rm <id>")
        return

    try:
        video_id = int(context.args[0])
    except ValueError:
        await update.message.reply_text("❌ ID должен быть числом")
        return

    video = get_showcase_video(video_id)
    if not video:
        await update.message.reply_text(f"❌ Видео ID={video_id} не найдено")
        return

    if remove_showcase_video(video_id):
        await update.message.reply_text(
            f"✅ Удалено из портфолио\n"
            f"ID: {video_id}\n"
            f"Task: {video['task_id']}"
        )
    else:
        await update.message.reply_text("❌ Ошибка удаления")


async def showcase_callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle showcase callback queries."""
    query = update.callback_query
    user_id = query.from_user.id

    if not is_admin(user_id):
        await query.answer("⛔ Только для администраторов", show_alert=True)
        return

    data = query.data

    if data.startswith("showcase_page:"):
        page = int(data.split(":")[1])
        limit = 15
        offset = (page - 1) * limit

        videos = get_showcase_videos(active_only=False, exclude_artifacts=False, limit=limit, offset=offset)
        total = get_showcase_videos_count(active_only=False, exclude_artifacts=False)
        total_pages = (total + limit - 1) // limit if total > 0 else 1

        text = f"📺 *Портфолио* (стр. {page}/{total_pages}, всего: {total})\n\n"

        for v in videos:
            status_icons = ""
            if not v.get("is_active"):
                status_icons += "🚫"
            if v.get("has_artifacts"):
                status_icons += "⚠️"

            rating = v.get("rating", 0)
            stars = "⭐" * rating if rating > 0 else "—"

            # Get product name from generation data
            product_name = ""
            gen = data_reader.get_generation(v['task_id'])
            if gen and gen.product_name:
                name = gen.product_name[:40]
                if len(gen.product_name) > 40:
                    name += "..."
                product_name = f" {name}"

            task_id_escaped = v['task_id'].replace('_', '\\_')
            text += f"`{v['id']}` {status_icons} {stars}{product_name} /v\\_{task_id_escaped}\n"

        # Navigation buttons
        keyboard = []
        nav_row = []
        if page > 1:
            nav_row.append(InlineKeyboardButton("⬅️", callback_data=f"showcase_page:{page-1}"))
        nav_row.append(InlineKeyboardButton(f"{page}/{total_pages}", callback_data="noop"))
        if page < total_pages:
            nav_row.append(InlineKeyboardButton("➡️", callback_data=f"showcase_page:{page+1}"))
        if nav_row:
            keyboard.append(nav_row)

        await query.message.edit_text(
            text,
            parse_mode="Markdown",
            reply_markup=InlineKeyboardMarkup(keyboard) if keyboard else None
        )
        await query.answer()

    elif data.startswith("showcase_add:"):
        task_id = data.split(":")[1]

        # Get generation info
        gen = data_reader.get_generation(task_id)
        if not gen or not gen.has_video or not gen.video_path:
            await query.answer("❌ Видео не найдено", show_alert=True)
            return

        # Check if already in showcase
        existing = get_showcase_video_by_task(task_id)
        if existing:
            await query.answer(f"⚠️ Уже в портфолио (ID: {existing['id']})", show_alert=True)
            return

        # Add to showcase
        video_id = add_showcase_video(
            video_path=gen.video_path,
            task_id=task_id,
            product_id=gen.product_id,
            rating=0,
            has_artifacts=False,
            added_by=user_id
        )

        if video_id:
            await query.answer(f"✅ Добавлено (ID: {video_id})")
            # Update buttons only (don't resend video)
            new_showcase_entry = get_showcase_video_by_task(task_id)
            keyboard = build_video_keyboard(task_id, gen.product_id, new_showcase_entry)
            await query.message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(keyboard))
        else:
            await query.answer("❌ Ошибка добавления", show_alert=True)

    elif data.startswith("showcase_remove:"):
        task_id = data.split(":")[1]

        showcase_entry = get_showcase_video_by_task(task_id)
        if not showcase_entry:
            await query.answer("❌ Не найдено в портфолио", show_alert=True)
            return

        # Get product_id before removing
        gen = data_reader.get_generation(task_id)
        product_id = gen.product_id if gen else showcase_entry.get("product_id", "?")

        if remove_showcase_video(showcase_entry["id"]):
            await query.answer(f"✅ Удалено (ID: {showcase_entry['id']})")
            # Update buttons only (don't resend video)
            keyboard = build_video_keyboard(task_id, product_id, None)
            await query.message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(keyboard))
        else:
            await query.answer("❌ Ошибка удаления", show_alert=True)

    elif data.startswith("showcase_rate:"):
        parts = data.split(":")
        task_id = parts[1]
        rating = int(parts[2])

        showcase_entry = get_showcase_video_by_task(task_id)
        if not showcase_entry:
            await query.answer("❌ Не найдено в портфолио", show_alert=True)
            return

        if set_showcase_video_rating(showcase_entry["id"], rating):
            await query.answer(f"✅ Рейтинг: {'⭐' * rating}")
            # Update buttons only (don't resend video)
            gen = data_reader.get_generation(task_id)
            product_id = gen.product_id if gen else showcase_entry.get("product_id", "?")
            updated_entry = get_showcase_video_by_task(task_id)
            keyboard = build_video_keyboard(task_id, product_id, updated_entry)
            await query.message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(keyboard))
        else:
            await query.answer("❌ Ошибка", show_alert=True)


async def admin_callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """Handle admin callback queries."""
    query = update.callback_query
    user_id = query.from_user.id

    if not is_admin(user_id):
        await query.answer("⛔ Только для администраторов", show_alert=True)
        return

    data = query.data
    db_path = get_current_db_path(context)

    if data.startswith("admin_users:"):
        page = int(data.split(":")[1])
        context.args = [str(page)]
        await admin_users(update, context)
        await query.answer()

    elif data.startswith("admin_grant:"):
        parts = data.split(":")
        target_id = int(parts[1])
        amount = int(parts[2])
        reason = "Quick grant via button"

        if credit_tokens(target_id, amount, "admin_grant", reason, initiated_by=user_id, db_path=db_path):
            new_balance = get_balance(target_id, db_path)
            await query.answer(f"✅ +{amount} токенов. Баланс: {new_balance}")
        else:
            await query.answer("❌ Ошибка", show_alert=True)

    elif data.startswith("admin_revoke:"):
        parts = data.split(":")
        target_id = int(parts[1])
        amount = int(parts[2])
        reason = "Quick revoke via button"

        if revoke_tokens(target_id, amount, reason, initiated_by=user_id, db_path=db_path):
            new_balance = get_balance(target_id, db_path)
            await query.answer(f"✅ -{amount} токенов. Баланс: {new_balance}")
        else:
            await query.answer("❌ Недостаточно токенов", show_alert=True)

    elif data.startswith("admin_block:"):
        target_id = int(data.split(":")[1])
        if block_user(target_id, "Blocked via button", initiated_by=user_id, db_path=db_path):
            await query.answer("🚫 Заблокирован")
        else:
            await query.answer("❌ Ошибка", show_alert=True)

    elif data.startswith("admin_unblock:"):
        target_id = int(data.split(":")[1])
        if unblock_user(target_id, db_path):
            await query.answer("✅ Разблокирован")
        else:
            await query.answer("❌ Ошибка", show_alert=True)


async def post_init(application):
    """Set up bot commands menu after initialization."""
    commands = [
        # User commands
        BotCommand("products", "📦 Список товаров"),
        BotCommand("stats", "📊 Статистика"),
        BotCommand("help", "❓ Справка"),
        # Showcase commands
        BotCommand("showcase", "📺 Портфолио видео"),
        BotCommand("showcase_add", "➕ Добавить в портфолио"),
        BotCommand("showcase_rm", "➖ Убрать из портфолио"),
        # Admin commands
        BotCommand("dev", "🔧 DEV база данных"),
        BotCommand("prod", "🚀 PROD база данных"),
        BotCommand("users", "👥 Список пользователей"),
        BotCommand("user", "👤 Детали пользователя"),
        BotCommand("usearch", "🔍 Поиск пользователя"),
        BotCommand("grant", "➕ Начислить токены"),
        BotCommand("revoke", "➖ Списать токены"),
        BotCommand("block", "🚫 Заблокировать"),
        BotCommand("unblock", "✅ Разблокировать"),
        BotCommand("dropuser", "🗑 Удалить пользователя"),
        BotCommand("txns", "📜 Транзакции"),
        BotCommand("tokenstats", "📊 Статистика токенов"),
        BotCommand("promo", "🎁 Промо управление"),
        BotCommand("giveaway_start", "🎉 Запустить раздачу токенов"),
        BotCommand("giveaway_stop", "🛑 Остановить раздачу"),
        BotCommand("giftcamp", "🎁 Кампании подарков"),
        BotCommand("giftpack", "🎁 Подарок пользователю"),
        BotCommand("giftpacks", "📊 Статистика подарков"),
        BotCommand("giftbulk", "📦 Массовая отправка подарков"),
        BotCommand("refstats", "📊 Статистика источников"),
        BotCommand("survey", "📋 Управление опросами"),
        BotCommand("gift", "🎬 Отправить видео пользователю"),
    ]
    await application.bot.set_my_commands(commands)
    logger.info("Bot commands menu set")


def main():
    """Start the bot."""
    if not BOT_TOKEN:
        logger.error("VIEWER_BOT_TOKEN not set")
        return

    if not ALL_ALLOWED:
        logger.warning("No allowed users configured")

    logger.info(f"Starting Video Viewer Bot. Allowed users: {ALL_ALLOWED}")

    from telegram.request import HTTPXRequest

    class RetryHTTPXRequest(HTTPXRequest):
        """HTTPXRequest with retry on ConnectError (SOCKS5 proxy hiccups)."""
        MAX_RETRIES = 3
        RETRY_DELAY = 2  # seconds

        async def do_request(self, *args, **kwargs):
            last_exc = None
            for attempt in range(self.MAX_RETRIES):
                try:
                    return await super().do_request(*args, **kwargs)
                except NetworkError as e:
                    if "ConnectError" in str(e):
                        last_exc = e
                        if attempt < self.MAX_RETRIES - 1:
                            logger.warning(f"Telegram ConnectError (attempt {attempt + 1}/{self.MAX_RETRIES}), retrying in {self.RETRY_DELAY}s...")
                            await asyncio.sleep(self.RETRY_DELAY)
                            continue
                    raise
            raise last_exc

    request_kwargs = dict(
        connect_timeout=30.0,
        read_timeout=60.0,
        write_timeout=60.0,
        pool_timeout=10.0,
    )
    if TG_PROXY:
        request_kwargs["proxy"] = TG_PROXY
        logger.info(f"Using TG_PROXY: {TG_PROXY}")
    request = RetryHTTPXRequest(**request_kwargs)
    get_updates_request = RetryHTTPXRequest(**request_kwargs)
    app = Application.builder().token(BOT_TOKEN).request(request).get_updates_request(get_updates_request).post_init(post_init).build()

    # Initialize databases
    init_db(DB_PATH_DEV)
    init_showcase_db()

    # Command handlers
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("help", start))
    app.add_handler(CommandHandler("products", products))
    app.add_handler(CommandHandler("product", product))
    app.add_handler(CommandHandler("video", video))
    app.add_handler(CommandHandler("stats", stats))
    app.add_handler(CommandHandler("search", search_command))
    app.add_handler(CommandHandler("rate", rate))
    app.add_handler(CommandHandler("comment", comment))

    # Admin commands for token management
    app.add_handler(CommandHandler("dev", admin_dev))
    app.add_handler(CommandHandler("prod", admin_prod))
    app.add_handler(CommandHandler("users", admin_users))
    app.add_handler(CommandHandler("user", admin_user))
    app.add_handler(CommandHandler("grant", admin_grant))
    app.add_handler(CommandHandler("revoke", admin_revoke))
    app.add_handler(CommandHandler("block", admin_block))
    app.add_handler(CommandHandler("unblock", admin_unblock))
    app.add_handler(CommandHandler("dropuser", admin_dropuser))
    app.add_handler(CommandHandler("txns", admin_txns))
    app.add_handler(CallbackQueryHandler(handle_txns_page, pattern=r"^txns:\d+"))
    app.add_handler(CommandHandler("tokenstats", admin_tokenstats))
    app.add_handler(CommandHandler("promo", admin_promo))
    app.add_handler(CommandHandler("giveaway_start", admin_giveaway_start))
    app.add_handler(CommandHandler("giveaway_stop", admin_giveaway_stop))
    app.add_handler(CommandHandler("usearch", admin_search))
    app.add_handler(CommandHandler("refstats", admin_refstats))
    app.add_handler(CommandHandler("gift", admin_gift))

    # Gift pack admin commands
    app.add_handler(CommandHandler("giftcamp", admin_giftcamp))
    app.add_handler(CommandHandler("giftpack", admin_giftpack))
    app.add_handler(CommandHandler("giftpacks", admin_giftpacks))
    app.add_handler(CommandHandler("giftbulk", admin_giftbulk))
    app.add_handler(CallbackQueryHandler(handle_confirm_giftbulk, pattern="^confirm_giftbulk$"))
    app.add_handler(CallbackQueryHandler(handle_cancel_giftbulk, pattern="^cancel_giftbulk$"))

    # Survey admin command
    app.add_handler(CommandHandler("survey", admin_survey))

    # Showcase admin commands
    app.add_handler(CommandHandler("showcase", admin_showcase))
    app.add_handler(CommandHandler("showcase_add", admin_showcase_add))
    app.add_handler(CommandHandler("showcase_rm", admin_showcase_rm))

    # Shortcut commands /p_<id> and /v_<id>
    app.add_handler(MessageHandler(filters.Regex(r'^/p_(?:(?:wb|avito|ozon)_)?\d+'), shortcut_product))
    app.add_handler(MessageHandler(filters.Regex(r'^/v_[\w]+'), shortcut_video))

    # Showcase callback query handler (must be before general callback_handler)
    app.add_handler(CallbackQueryHandler(showcase_callback_handler, pattern="^showcase_"))

    # Admin callback query handler (must be before general callback_handler)
    app.add_handler(CallbackQueryHandler(admin_callback_handler, pattern="^admin_"))

    # Callback query handler for inline buttons
    app.add_handler(CallbackQueryHandler(callback_handler))

    # Text message handler (for natural language search)
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, text_message))

    # Run
    app.run_polling(allowed_updates=Update.ALL_TYPES)


if __name__ == "__main__":
    main()
