import logging
from services.link_pool.assigner import assign_link_for_order
from database.models import LinkType
from bot.keyboards.admin import categories_manage_keyboard
from database.queries import get_all_categories
from bot.keyboards.admin import link_category_keyboard
from database.queries import get_all_links, get_plan_by_id, get_category_by_id
from bot.keyboards.admin import back_to_link_pool_menu
from aiogram.utils.keyboard import InlineKeyboardBuilder
from database.queries import get_active_orders, search_users, update_order_expiration
from sqlalchemy import text
from database.queries import get_link_for_order, get_plan_by_id, get_order_by_id
from database.queries import get_users_by_type, get_blocked_user_ids, unblock_user
import asyncio
from aiogram.exceptions import (
    TelegramForbiddenError,
    TelegramRetryAfter,
    TelegramBadRequest
)
from database.queries import mark_user_blocked



from aiogram.types import CallbackQuery
from aiogram.exceptions import TelegramBadRequest
import math
import logging
from database.queries import (
    expire_order,
    delete_order_by_id,  
    delete_expired_order_and_link,
    set_order_status,
    add_gift_credit,
    get_pending_orders,
    use_discount_code,
    log_discount_usage,
    get_discount_code,
    )
from bot.keyboards.admin import build_pending_orders_keyboard, admin_main_menu
from services.link_pool.assigner import assign_link_for_order






from database.queries import (
    add_category,
    add_plan,
    add_link_to_pool,
    get_all_categories,
    get_link_for_order,
    get_plans_by_category,
    get_all_links,
    delete_link_by_id,
    update_link_field,
    delete_category,
    update_category_field,
    delete_plan,
    update_plan_field,
    get_pending_orders,
    get_order_by_id,
    set_order_status,
    get_plan_by_id,
)
from database.models import PaymentStatus

from aiogram import F, Router, types
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.states.states import AdminOrderStates
from bot.keyboards.admin import admin_active_users_kb

from config import ADMINS
from bot.keyboards.user import build_receipt_keyboard
from bot.keyboards.admin import (
    admin_main_menu,
    category_type_keyboard,
    categories_manage_keyboard,
    category_actions_keyboard,
    category_edit_field_keyboard,
    plans_keyboard,
    plan_actions_keyboard,
    edit_field_keyboard,
    build_pending_orders_keyboard,
    order_action_keyboard,
    link_pool_menu,
    back_to_link_pool_menu,
)
from database.queries import expire_order
from aiogram.exceptions import TelegramBadRequest
from database.queries import get_orders_without_link
from bot.keyboards.admin import no_link_order_actions_keyboard
from database.queries import get_links_with_capacity, assign_specific_link_to_order
from aiogram.enums import ParseMode
from database.queries import get_links_by_category
from bot import bot  # 👈 اینجا وارد کن
from database.queries import get_subscribers_by_link_id
from aiogram.types import CallbackQuery
from aiogram.types import InlineKeyboardMarkup
from database.queries import search_links_by_keyword
# بالای فایل (در کنار سایر ایمپورت‌ها)
from database.models import PaymentStatus
from bot.keyboards.admin import order_action_keyboard, no_link_order_actions_keyboard

from bot.utils.helpers import calculate_discount_based_on_date
from database.queries       import set_order_price_discount
import math
from bot.keyboards.admin import (
    admin_main_menu,
    monthly_report_choice_keyboard,
)
from database.queries import get_monthly_sales_report
from aiogram.types import InputFile
from database.queries import get_link_by_url

import csv
import io
from datetime import datetime, timedelta

from aiogram.types import BufferedInputFile
from sqlalchemy import select, func

from database.db import async_session
from database.models import Order, Plan, PlanCategory
from aiogram import F
from aiogram.types import CallbackQuery
from bot.keyboards.admin import admin_main_menu
from database.queries import get_all_active_orders
from database.db import async_session
import jdatetime  # کتابخانهٔ تبدیل تاریخ
from datetime import datetime
from database.queries import get_user_active_order
from database.db import async_session
from database.queries import (
    get_order_by_id,
    assign_specific_link_to_order,
    get_link_url_by_id,
)
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import InlineKeyboardButton
from database.queries import get_order_by_id, get_plan_by_id
import jdatetime
from database.queries import get_order_by_id, get_plan_by_id
import jdatetime
from bot.keyboards.admin import back_to_user_subs_kb
from aiogram.exceptions import TelegramBadRequest
from aiogram import F
from aiogram.types import CallbackQuery, Message
from aiogram.fsm.context import FSMContext
from bot.states.states import AdminNotificationStates
from database.queries import get_active_orders  # لیست سفارش‌های فعال
from database.models import User                # مدل کاربر
from sqlalchemy.ext.asyncio import AsyncSession
from database.db import async_session  
from aiogram.filters import StateFilter
from bot.states.states import PaymentEditStates





# ───────────── Setup ─────────────
logger = logging.getLogger(__name__)
router = Router()

# ───────────── State Groups ────────────────
class AdminStates(StatesGroup):
    # حالت برای دریافت URL جدید از ادمین
    editing_link_url = State()
class LinkSearchStates(StatesGroup):
    waiting_for_query = State()

class SendLinkMessage(StatesGroup):
    waiting_for_message = State()


class AddCategory(StatesGroup):
    waiting_for_title = State()
    waiting_for_type = State()

class AddPlan(StatesGroup):
    waiting_for_category    = State()
    waiting_for_name        = State()
    waiting_for_price       = State()
    waiting_for_user_limit  = State()       # ← حالت جدید
    waiting_for_description = State()

class EditPlan(StatesGroup):
    waiting_for_value = State()

class EditCategory(StatesGroup):
    waiting_for_value = State()

class RejectOrder(StatesGroup):
    waiting_for_reason = State()

class Purchase(StatesGroup):
    waiting_receipt = State()

class AddLink(StatesGroup):
    waiting_for_url      = State()
    waiting_for_capacity = State()
    waiting_for_cat_link = State()
    waiting_for_plan     = State()
    waiting_for_type     = State()
    waiting_for_label    = State()

class EditLink(StatesGroup):
    waiting_for_field              = State()
    waiting_for_value              = State()
    waiting_for_category_selection = State()
    waiting_for_plan_selection     = State()


from aiogram.fsm.state import StatesGroup, State

class AdminSearchStates(StatesGroup):
    waiting_for_query = State()


class AdminOrderStates(StatesGroup):
    waiting_for_new_expiry = State()


class SendMessageToUser(StatesGroup):
    waiting_for_text = State()


class EditUserGift(StatesGroup):
    waiting_for_amount = State()
    
class BulkLinkReplaceStates(StatesGroup):
    waiting_for_old_text = State()
    waiting_for_new_text = State()
    waiting_for_confirm = State()


# ───────────── Helpers ─────────────
def is_admin(user_id: int) -> bool:
    return user_id in ADMINS

# ───────────── Handlers ─────────────
# ─────────── جزئیات سفارش (بدون لینک/با لینک) ───────────
from database.models import PaymentStatus
from database.queries import get_plan_by_id, get_order_by_id, get_orders_without_link
from bot.keyboards.admin import order_action_keyboard, no_link_order_actions_keyboard, admin_main_menu
from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram import F, Router
from aiogram.fsm.context import FSMContext



import math
import jdatetime
from database.models import PaymentStatus
from database.queries import get_plan_by_id
from bot.keyboards.admin import order_action_keyboard, no_link_order_actions_keyboard


@router.callback_query(F.data == "admin:no_link_orders")
async def no_link_orders(cb: types.CallbackQuery):
    orders = await get_orders_without_link()
    if not orders:
        await cb.message.edit_text("✅ هیچ سفارشی بدون لینک نیست.", reply_markup=admin_main_menu())
        await cb.answer()
        return

    kb = InlineKeyboardBuilder()
    for o in orders:
        plan = await get_plan_by_id(o.plan_id)
        plan_name = plan.name if plan else "نامشخص"
        kb.button(
            text=f"#{o.id} کاربر:{o.user_id} پلن:{plan_name}",
            callback_data=f"admin:order:{o.id}"
        )
    kb.button(text="🔙 بازگشت", callback_data="admin:back")
    kb.adjust(1)

    await cb.message.edit_text("❌ سفارشات تأییدشده بدون لینک:", reply_markup=kb.as_markup())
    await cb.answer()

# — /admin —
@router.message(Command("admin"))
async def admin_panel(msg: types.Message):
    if not is_admin(msg.from_user.id):
        return await msg.answer("⛔️ شما دسترسی ادمین نداری.")
    await msg.answer("🛠 پنل ادمین:", reply_markup=admin_main_menu())

@router.callback_query(F.data == "admin:back")
async def admin_back(cb: types.CallbackQuery, state: FSMContext):
    await state.clear()
    await cb.message.edit_text("🛠 پنل ادمین:", reply_markup=admin_main_menu())
    await cb.answer()

# — Add Category —
@router.callback_query(F.data == "admin:add_category")
async def add_category_start(cb: types.CallbackQuery, state: FSMContext):
    await cb.message.edit_text("📝 عنوان دسته رو بگو:")
    await state.set_state(AddCategory.waiting_for_title)
    await cb.answer()

@router.message(AddCategory.waiting_for_title)
async def add_category_title(msg: types.Message, state: FSMContext):
    await state.update_data(title=msg.text.strip())
    await msg.answer("📌 نوع رو انتخاب کن:", reply_markup=category_type_keyboard())
    await state.set_state(AddCategory.waiting_for_type)

@router.callback_query(AddCategory.waiting_for_type)
async def add_category_type(cb: types.CallbackQuery, state: FSMContext):
    data = await state.get_data()
    cat_type = "حجمی" if cb.data.endswith("volume") else "نامحدود"
    await add_category(type=cat_type, title=data["title"])
    await state.clear()
    await cb.message.edit_text("✅ دسته اضافه شد.", reply_markup=admin_main_menu())
    await cb.answer()

# — Add Plan —
@router.callback_query(F.data == "admin:add_plan")
async def add_plan_start(cb: types.CallbackQuery, state: FSMContext):
    cats = await get_all_categories()
    if not cats:
        return await cb.answer("اول یه دسته بساز.", show_alert=True)

    kb = InlineKeyboardMarkup(
        inline_keyboard=[
            [InlineKeyboardButton(text=f"{c.title} ({c.type})", callback_data=f"admin:catadd:{c.id}")]
            for c in cats
        ] + [[InlineKeyboardButton(text="🔙 بازگشت", callback_data="admin:back")]]
    )

    await cb.message.edit_text("یه دسته انتخاب کن:", reply_markup=kb)
    await state.set_state(AddPlan.waiting_for_category)
    await cb.answer()


@router.callback_query(AddPlan.waiting_for_category, F.data.startswith("admin:catadd:"))
async def add_plan_choose_cat(cb: types.CallbackQuery, state: FSMContext):
    await state.update_data(category_id=int(cb.data.split(":")[2]))
    await cb.message.edit_text("📝 اسم پلن:")
    await state.set_state(AddPlan.waiting_for_name)
    await cb.answer()

@router.message(AddPlan.waiting_for_name)
async def add_plan_name(msg: types.Message, state: FSMContext):
    await state.update_data(name=msg.text.strip())
    await msg.answer("💵 قیمت (تومان):")
    await state.set_state(AddPlan.waiting_for_price)

@router.message(AddPlan.waiting_for_price)
async def add_plan_price(msg: types.Message, state: FSMContext):
    if not msg.text.isdigit():
        return await msg.answer("⛔️ فقط عدد بنویس.")
    await state.update_data(price=float(msg.text))
    await msg.answer("👥 حداکثر تعداد کاربر (عدد بنویس):")
    await state.set_state(AddPlan.waiting_for_user_limit)

@router.message(AddPlan.waiting_for_user_limit)
async def add_plan_user_limit(msg: types.Message, state: FSMContext):
    if not msg.text.isdigit():
        return await msg.answer("⛔️ فقط عدد بنویس.")
    await state.update_data(user_limit=int(msg.text))
    await msg.answer("📝 توضیح کوتاه یا '-' برای خالی:")
    await state.set_state(AddPlan.waiting_for_description)

@router.message(AddPlan.waiting_for_description)
async def add_plan_desc(msg: types.Message, state: FSMContext):
    data = await state.get_data()
    desc = None if msg.text.strip() == "-" else msg.text.strip()
    await add_plan(
        name=data["name"],
        type="عمومی",
        category_id=data["category_id"],
        price=data["price"],
        description=desc or "",
        user_limit=data["user_limit"],
    )
    await state.clear()
    await msg.answer("✅ پلن ثبت شد.", reply_markup=admin_main_menu())

# — List & Manage Categories —
@router.callback_query(F.data == "admin:list_categories")
async def list_categories(cb: types.CallbackQuery):
    cats = await get_all_categories()
    await cb.message.edit_text("🔖 دسته‌بندی‌ها:", reply_markup=categories_manage_keyboard(cats))
    await cb.answer()

from database.queries import get_category_by_id
from bot.keyboards.admin import category_actions_keyboard

@router.callback_query(F.data.startswith("admin:editcat:"))
async def category_actions(cb: types.CallbackQuery):
    cat_id = int(cb.data.split(":")[2])
    # ← دریافت شیء دسته از دیتابیس
    cat = await get_category_by_id(cat_id)
    # ← پاس دادن شیء به کیبورد
    await cb.message.edit_text("چی می‌خوای؟", reply_markup=category_actions_keyboard(cat))
    await cb.answer()


@router.callback_query(F.data.startswith("admin:cat_del:"))
async def category_delete(cb: types.CallbackQuery):
    cat_id = int(cb.data.split(":")[2])
    await delete_category(cat_id)
    cats = await get_all_categories()
    await cb.message.edit_text("✅ حذف شد.", reply_markup=categories_manage_keyboard(cats))
    await cb.answer()

@router.callback_query(F.data.startswith("admin:cat_edit:"))
async def category_edit_fields(cb: types.CallbackQuery, state: FSMContext):
    cat_id = int(cb.data.split(":")[2])
    await state.update_data(category_id=cat_id)
    await cb.message.edit_text("کدوم فیلد؟", reply_markup=category_edit_field_keyboard(cat_id))
    await state.set_state(EditCategory.waiting_for_value)
    await cb.answer()

@router.callback_query(F.data.startswith("admin:cat_field:"))
async def category_field_selected(cb: types.CallbackQuery, state: FSMContext):
    _, _, field, cat_id = cb.data.split(":")
    await state.update_data(field=field)
    prompt = "عنوان جدید:" if field == "title" else "نوع جدید (حجمی/نامحدود):"
    await cb.message.edit_text(prompt)
    await cb.answer()

@router.message(EditCategory.waiting_for_value)
async def category_receive_value(msg: types.Message, state: FSMContext):
    data = await state.get_data()
    await update_category_field(data["category_id"], data["field"], msg.text.strip())
    cats = await get_all_categories()
    await state.clear()
    await msg.answer("✅ ویرایش شد.", reply_markup=categories_manage_keyboard(cats))

# — View Plans in Category —
@router.callback_query(F.data.startswith("admin:catview:"))
async def view_plans(cb: types.CallbackQuery):
    cat_id = int(cb.data.split(":")[2])
    plans = await get_plans_by_category(cat_id)
    await cb.message.edit_text("📦 پلن‌ها:", reply_markup=plans_keyboard(plans, cat_id))
    await cb.answer()

# — Plan Details & Actions —
@router.callback_query(F.data.startswith("admin:plan:"))
async def plan_details(cb: types.CallbackQuery):
    _, _, plan_id, cat_id = cb.data.split(":")
    plan_id, cat_id = int(plan_id), int(cat_id)
    plan = next((p for p in await get_plans_by_category(cat_id) if p.id == plan_id), None)
    if not plan:
        return await cb.answer("پلن پیدا نشد.", show_alert=True)
    text = (
        f"<b>{plan.name}</b>\n"
        f"💵 {int(plan.price)} تومان\n"
        f"👥 حداکثر کاربر: {plan.user_limit}\n"
        f"📝 {plan.description or '-'}"
    )
    plan = await get_plan_by_id(plan_id)  # این خط باید بالاتر از این باشد
    await cb.message.edit_text(
        text,
        reply_markup=plan_actions_keyboard(plan_id, cat_id, is_full=plan.is_full)
    )

    await cb.answer()

@router.callback_query(F.data.startswith("admin:plan_del:"))
async def plan_delete(cb: types.CallbackQuery):
    _, _, plan_id, cat_id = cb.data.split(":")
    await delete_plan(int(plan_id))
    await cb.message.edit_text(
        "✅ پلن حذف شد.",
        reply_markup=plans_keyboard(await get_plans_by_category(int(cat_id)), int(cat_id))
    )
    await cb.answer()

@router.callback_query(F.data.startswith("admin:plan_edit:"))
async def plan_edit_field_choose(cb: types.CallbackQuery, state: FSMContext):
    _, _, plan_id, cat_id = cb.data.split(":")
    await state.update_data(plan_id=int(plan_id), cat_id=int(cat_id))
    await cb.message.edit_text("کدوم فیلد؟", reply_markup=edit_field_keyboard(int(plan_id), int(cat_id)))
    await cb.answer()

@router.callback_query(F.data.startswith("admin:editfield:"))
async def plan_field_cb(cb: types.CallbackQuery, state: FSMContext):
    _, _, field, plan_id, cat_id = cb.data.split(":")
    await state.update_data(field=field)
    prompt_map = {
        "name": "عنوان جدید:",
        "price": "قیمت جدید (عدد):",
        "description": "توضیح جدید:",
        "user_limit": "تعداد کاربر جدید (عدد):",
    }
    await cb.message.edit_text(prompt_map[field])
    await state.set_state(EditPlan.waiting_for_value)
    await cb.answer()

from aiogram.enums import ParseMode
import html
import traceback
from aiogram.exceptions import TelegramBadRequest

@router.message(EditPlan.waiting_for_value)
async def plan_field_value(msg: types.Message, state: FSMContext):
    data = await state.get_data()
    field = data.get("field")
    plan_id = data.get("plan_id")
    cat_id = data.get("cat_id")

    if not field or plan_id is None or cat_id is None:
        await state.clear()
        return await msg.answer("⚠️ خطا در داده‌ها. لطفاً دوباره تلاش کن.")

    raw = msg.text.strip()

    # اعتبارسنجی
    if field in ("price", "user_limit"):
        if not raw.isdigit():
            return await msg.answer("⛔️ فقط عدد وارد کن.")
        value = float(raw) if field == "price" else int(raw)
    else:
        value = raw

    # ذخیره در دیتابیس
    try:
        await update_plan_field(plan_id, field, value)
    except Exception:
        logger.exception("خطا در update_plan_field")
        await state.clear()
        return await msg.answer("❌ خطا در ذخیره‌سازی تغییرات.")

    # گرفتن مجدد پلن‌های همان دسته (دقیقاً مثل دسته‌بندی‌ها)
    plans = await get_plans_by_category(cat_id)

    await state.clear()

    # پیام تأیید + لیست پلن‌ها
    await msg.answer(
        "✅ ویرایش پلن با موفقیت انجام شد.\n\n📦 لیست پلن‌ها:",
        reply_markup=plans_keyboard(plans, cat_id)
    )





# ─────────── ایمپورت‌ها ───────────
from database.queries import (
    add_gift_credit,
    get_order_by_id,
    get_plan_by_id,
    expire_order,
    get_pending_orders,
    set_order_status,            # ← اضافه
)
from database.db import async_session
from database.models import Order, PaymentStatus
from aiogram.exceptions import TelegramBadRequest
from sqlalchemy import delete
from datetime import datetime, timedelta
import math
from aiogram import F
from aiogram.types import (
    CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, InputFile
)
import jdatetime
from pathlib import Path
import json
import traceback

# کیبوردها و کمک‌کننده‌های مورد نیاز
from bot.keyboards.admin import (
    order_action_keyboard,
    build_pending_orders_keyboard,
    admin_main_menu,
)

# assigner برای اختصاص لینک بعد از تایید
from services.link_pool.assigner import assign_link_for_order

# ─────────── تابع کمکی حذف سفارش ───────────
async def delete_order(order_id: int):
    async with async_session() as session:
        await session.execute(delete(Order).where(Order.id == order_id))
        await session.commit()
        print(f"🗑️ سفارش {order_id} با موفقیت حذف شد.")


# ─────────── تابع کمکی برای یافتن/خواندن رسید از DB یا فایل‌های لوکال ───────────
def _find_local_receipt(order) -> dict | None:
    """
    اگر فیلدهای محلی در مدل Order وجود نداشته باشند،
    به دنبال فایل JSON داخل پوشه receipts بگرد (pattern: {user_id}_{order_id}_*.json)
    و در صورت پیدا شدن دادهٔ receipt را برگردان.
    خروجی: dict با کلیدهای: receipt_type, receipt_value, saved_file (ممکن None باشد)
    یا None اگر چیزی یافت نشد.
    """
    receipts_folder = Path("receipts")
    if not receipts_folder.exists():
        return None

    pattern = f"{order.user_id}_{order.id}_*.json"
    files = sorted(receipts_folder.glob(pattern), reverse=True)  # جدیدترین اول
    for f in files:
        try:
            with f.open("r", encoding="utf-8") as fh:
                data = json.load(fh)
            if "receipt_type" in data and "receipt_value" in data:
                return {
                    "receipt_type": data.get("receipt_type"),
                    "receipt_value": data.get("receipt_value"),
                    "saved_file": data.get("saved_file"),
                }
        except Exception:
            # فایل خراب یا خواندن مشکل داشت → ادامه میدیم
            continue
    return None


# ─────────── لیست سفارش‌های معوق ───────────
# ====== states جدید برای جستجوی pending orders ======
from aiogram.fsm.state import State, StatesGroup

class AdminPendingSearchStates(StatesGroup):
    waiting_for_query = State()

# ====== handler عمومی برای نمایش یک صفحه از pending orders ======
from aiogram import F
from database.queries import get_pending_orders_page, count_pending_orders
from bot.keyboards.admin import build_pending_orders_keyboard
from aiogram.exceptions import TelegramBadRequest

PAGE_SIZE = 10

async def _show_pending_page(cb: CallbackQuery, page: int = 1, filters: dict | None = None):
    if not is_admin(cb.from_user.id):
        return await cb.answer("⛔️ دسترسی نداری.", show_alert=True)

    if page < 1:
        page = 1
    offset = (page - 1) * PAGE_SIZE

    try:
        # دریافت داده‌ها
        orders = await get_pending_orders_page(PAGE_SIZE, offset, filters)
        total = await count_pending_orders(filters)

        if not orders:
            # اگر فیلتر هست و چیزی نیست → اطلاع بدیم و دکمهٔ بازگشت
            if filters:
                try:
                    await cb.message.edit_text(
                        "⚠️ موردی یافت نشد. ❌",
                        reply_markup=build_pending_orders_keyboard([], 1, PAGE_SIZE, 0)
                    )
                except TelegramBadRequest:
                    await cb.bot.send_message(cb.from_user.id, "⚠️ موردی یافت نشد.")
                return await cb.answer()

            # اگر کلاً چیز pending نیست، نمایش منوی اصلی
            try:
                await cb.message.edit_text("🧾 هیچ سفارش معوقی نیست.", reply_markup=admin_main_menu())
            except TelegramBadRequest:
                await cb.bot.send_message(cb.from_user.id, "🧾 هیچ سفارش معوقی نیست.", reply_markup=admin_main_menu())
            return await cb.answer()

        kb = build_pending_orders_keyboard(orders, page, PAGE_SIZE, total, query=filters)

        # تلاش برای edit متن پیام فعلی؛ اگر ممکن نبود پیام جدید ارسال کن
        text_lines = [f"🧾 سفارش‌های معوق (صفحه {page}):\n"]
        for o in orders:
            uname = getattr(o.user, "username", None)
            user_label = f"@{uname}" if uname else f"ID:{o.user_id}"
            plan_name = getattr(o.plan, "name", "-") if getattr(o, "plan", None) else "-"
            text_lines.append(f"• #{o.id} — {user_label} — {plan_name}")

        text = "\n".join(text_lines)

        try:
            if getattr(cb, "message", None):
                await cb.message.edit_text(text, reply_markup=kb, parse_mode="HTML")
            else:
                await cb.bot.send_message(cb.from_user.id, text, reply_markup=kb, parse_mode="HTML")
        except TelegramBadRequest as e:
            # fallback: send new message
            await cb.bot.send_message(cb.from_user.id, text, reply_markup=kb, parse_mode="HTML")
        await cb.answer()

    except Exception as e:
        logger.exception("خطا در نمایش pending page")
        try:
            await cb.answer("⚠️ خطا در واکشی سفارش‌ها.", show_alert=True)
        except Exception:
            pass

# ====== entry: نمایش صفحهٔ اول (یا همان callback قدیمی) ======
@router.callback_query(F.data == "admin:pending_orders")
async def pending_orders_entry(cb: CallbackQuery):
    await _show_pending_page(cb, page=1, filters=None)

# ====== صفحه‌بندی فقط (prev/next) ======
@router.callback_query(F.data.startswith("admin:pending:page:"))
async def pending_orders_page_cb(cb: CallbackQuery):
    try:
        parts = cb.data.split(":")
        page = int(parts[-1]) if parts[-1].isdigit() else 1
    except Exception:
        page = 1
    # در این callback ممکن است بخواهیم فیلترها را از callback نگه داریم
    # اما برای سادگی فعلی، callback های دکمه‌ها شامل filter نیستند.
    # اگر نیاز داشتی می‌تونیم callback_data رو طوری بسازیم که filter هم داخلش ذخیره شه.
    await _show_pending_page(cb, page=page, filters=None)

# ====== شروع جریان جستجو (انتخاب فیلد) ======
@router.callback_query(F.data == "admin:pending:search_start")
async def pending_search_start(cb: CallbackQuery):
    # نمایش گزینه‌ها: جستجو بر اساس user_id / order_code / username
    kb = InlineKeyboardMarkup(inline_keyboard=[
        [InlineKeyboardButton(text="🔎 بر اساس شناسه عددی (user_id)", callback_data="admin:pending:search_field:user_id")],
        [InlineKeyboardButton(text="🔎 بر اساس Order Code", callback_data="admin:pending:search_field:order_code")],
        [InlineKeyboardButton(text="🔎 بر اساس Username", callback_data="admin:pending:search_field:username")],
        [InlineKeyboardButton(text="❌ انصراف", callback_data="admin:pending:search_cancel")],
    ])
    await cb.message.edit_text("🔍 روش جستجو را انتخاب کن:", reply_markup=kb)
    await cb.answer()

@router.callback_query(F.data == "admin:pending:search_cancel")
async def pending_search_cancel(cb: CallbackQuery, state: FSMContext):
    await state.clear()
    await cb.answer()
    # بازگشت به صفحهٔ اول pending
    await _show_pending_page(cb, page=1, filters=None)

@router.callback_query(F.data.startswith("admin:pending:search_field:"))
async def pending_search_field_choose(cb: CallbackQuery, state: FSMContext):
    _, _, _, field = cb.data.split(":")
    # ذخیرهٔ فیلد در state و رفتن به حالت دریافت عبارت
    await state.update_data(pending_search_field=field)
    await state.set_state(AdminPendingSearchStates.waiting_for_query)

    prompts = {
        "user_id": "🔢 شناسه عددی کاربر را وارد کن (مثلاً: 123456789):",
        "order_code": "🔎 Order Code را وارد کن (مثلاً Mushak001):",
        "username": "🔎 نام کاربری (بدون @) یا قسمتی از آن را وارد کن:"
    }
    await cb.message.edit_text(prompts.get(field, "عبارت جستجو را وارد کن:"))
    await cb.answer()

# ====== پردازش عبارت جستجو ======
@router.message(AdminPendingSearchStates.waiting_for_query)
async def pending_search_receive(msg: Message, state: FSMContext):
    data = await state.get_data()
    field = data.get("pending_search_field")
    if not field:
        await msg.answer("⚠️ خطا: فیلد جستجو مشخص نیست. دوباره شروع کن.")
        await state.clear()
        return

    q = msg.text.strip().lstrip("@")
    if not q:
        await msg.answer("⚠️ عبارت خالی است. دوباره تلاش کن یا ❌ انصراف بزن.")
        return

    # ساخت فیلتر مناسب برای توابع DB
    filters = {}
    if field == "user_id":
        if not q.isdigit():
            await msg.answer("⛔️ برای user_id فقط عدد قبول میشه.")
            return
        filters["user_id"] = int(q)
    elif field == "order_code":
        filters["order_code"] = q
    elif field == "username":
        filters["username"] = q

    # حالا نمایش صفحهٔ اول نتایج جستجو
    # از همان تابع نمایش صفحه استفاده می‌کنیم ولی با filters
    # پاسخ را به چت ادمین (نه کاربر) می‌فرستیم
    # برای consistency از همان Callback-like flow استفاده می‌کنیم: پیام جدید می‌فرستیم
    try:
        # ساخت کیبورد و متن داخل _show_pending_page انجام می‌شود
        # اما چون اینجا msg است (نه callback)، می‌سازیم یک dummy callback-like object در حد نیاز:
        class _CB:
            def __init__(self, bot, from_user, message):
                self.bot = bot
                self.from_user = from_user
                self.message = message
            async def answer(self, *a, **k):
                try:
                    await msg.answer()  # noop-like
                except Exception:
                    pass

        fake_cb = _CB(msg.bot, msg.from_user, msg)
        await _show_pending_page(fake_cb, page=1, filters=filters)
    except Exception as e:
        logger.exception("خطا در جستجوی سفارش‌های معوق")
        await msg.answer("⚠️ خطا در جستجو. بعداً تلاش کن.")

    await state.clear()

# ====== clear search callback (بازگشت به حالت بدون فیلتر) ======
@router.callback_query(F.data == "admin:pending:clear_search")
async def pending_clear_search(cb: CallbackQuery):
    await cb.answer()
    await _show_pending_page(cb, page=1, filters=None)




def _is_valid_telegram_photo(value: str | None) -> bool:
    if not value or not isinstance(value, str):
        return False

    value = value.strip()

    # file_id تلگرام معمولاً این الگوها را دارد
    if value.startswith(("AgAC", "AQAD", "BAAC")):
        return True

    # URL معتبر
    if value.startswith(("http://", "https://")):
        return True

    return False


# ─────────── نمایش جزئیات یک سفارش معوق (وقتی ادمین از کیبورد یکی را انتخاب می‌کند) ───────────
# جایگزین کاملِ admin_order_detail_merged — نسخهٔ اصلاح شده برای جلوگیری از بلاک شدن ارسال پیام جدید
import html
import math
import jdatetime
import traceback
from pathlib import Path
from aiogram.types import CallbackQuery, InputFile

@router.callback_query(F.data.startswith("admin:order:"))
async def admin_order_detail_merged(cb: CallbackQuery):
    """
    هندلر ادغام‌شده با منطق امن برای:
      - تشخیص اینکه آیا باید پیام قبلی را edit کنیم یا پیام جدید ارسال کنیم
      - جلوگیری از حالتِ «هیچ‌چیز نمایش داده نمی‌شود» وقتی callback از لیستِ رسیدها می‌آید
    """
    # دسترسی ادمین
    if not is_admin(cb.from_user.id):
        return await cb.answer("⛔️ دسترسی نداری.", show_alert=True)

    # parse order id
    try:
        order_id = int(cb.data.split(":")[-1])
    except Exception:
        await cb.answer("⚠️ شناسه سفارش نامعتبر است.", show_alert=True)
        return

    order = await get_order_by_id(order_id)
    if not order:
        await cb.answer("⚠️ سفارش پیدا نشد یا حذف شده است.", show_alert=True)
        return

    plan = await get_plan_by_id(order.plan_id) if getattr(order, "plan_id", None) else None

    # 🔹 دریافت مستقیم و امن کاربر بر اساس user_id (جلوگیری از خطای DetachedInstanceError)
    user_id_val = getattr(order, "user_id", None)
    user_obj = None
    if user_id_val:
        try:
            from database.queries import get_user
            user_obj = await get_user(user_id_val)
        except Exception:
            user_obj = None

    raw_username = getattr(user_obj, "username", None) if user_obj else None
    if raw_username:
        username_val = f"@{html.escape(raw_username)}"
        user_name_val = html.escape(raw_username)
    else:
        username_val = "بدون یوزرنیم"
        user_name_val = "—"

    user_id_str = str(user_id_val) if user_id_val else "-"
    plan_name_val = html.escape(plan.name) if (plan and getattr(plan, "name", None)) else "-"

    # ساخت تاریخ‌های شمسی (در صورت وجود)
    start_str, expire_str = "", ""
    if getattr(order, "start_date", None) and getattr(order, "expiration_date", None):
        try:
            jal_start = jdatetime.date.fromgregorian(date=order.start_date.date())
            jal_expire = jdatetime.date.fromgregorian(date=order.expiration_date.date())
            start_str = jal_start.strftime("%Y/%m/%d")
            expire_str = jal_expire.strftime("%Y/%m/%d")
        except Exception:
            start_str, expire_str = "", ""

    # ---------- helper محلی: بررسی می‌کند آیا پیام فعلی همان پیامِ رسید است یا خیر ----------
    def _is_message_original_receipt(message, order_obj):
        try:
            if not message:
                return False
            content = ""
            if getattr(message, "caption", None):
                content = message.caption or ""
            elif getattr(message, "text", None):
                content = message.text or ""
            else:
                content = ""
            if not content:
                return False
            if f"#{getattr(order_obj, 'id', '')}" in content:
                return True
            oc = getattr(order_obj, "order_code", "") or ""
            if oc and oc in content:
                return True
            return False
        except Exception:
            return False

    # ---------- مسیر حالت 'pending' (منطق قدیمی حفظ شده) ----------
    if getattr(order, "payment_status", None) == PaymentStatus.pending:
        count = getattr(order, "user_count_requested", None) or getattr(order, "device_count", None) or 1
        try:
            count = int(count)
        except Exception:
            count = 1

        base_price = (getattr(plan, "price", 0) or getattr(order, "price", 0) or 0) * int(count)
        total_discount = getattr(order, "discount_percent", None) or 0.0

        is_unlimited = False
        try:
            from database.queries import get_category_by_id as _get_category_by_id
            try:
                cat = await _get_category_by_id(plan.category_id)
                if cat and getattr(cat, "type", None) == "نامحدود":
                    is_unlimited = True
            except Exception:
                is_unlimited = (getattr(plan, "volume_limit", None) is None or getattr(plan, "volume_limit", None) == 0)
        except Exception:
            is_unlimited = (getattr(plan, "volume_limit", None) is None or getattr(plan, "volume_limit", None) == 0)

        try:
            jal_day = jdatetime.date.fromgregorian(date=order.start_date.date()).day
        except Exception:
            try:
                jal_day = order.start_date.day
            except Exception:
                jal_day = None

        cycle_days = [1, 5, 10, 15, 20, 25]
        extra_discount = 10.0 if (is_unlimited and (jal_day is not None and (jal_day not in cycle_days))) else 0.0
        base_discount = (total_discount - extra_discount) if total_discount > extra_discount else 0.0

        try:
            raw_final = base_price * (1 - (float(total_discount) or 0.0) / 100.0)
            final_price = int(math.ceil(raw_final / 1000.0)) * 1000
        except Exception:
            final_price = int(getattr(order, "final_price", 0) or 0)

        # ساخت متن رسید (با مشخصات تفکیک‌شده کاربر در خطوط مجزا)
        text_lines = [
            f"🧾 رسید سفارش #{getattr(order, 'id', getattr(order, 'order_code', '-'))}",
            f"👤 نام کاربر: {user_name_val}",
            f"🌐 یوزرنیم: {username_val}",
            f"🔢 آیدی عددی: <code>{user_id_str}</code>",
            f"📦 پلن: {plan_name_val}",
            f"👥 تعداد دستگاه: {count}",
            f"💰 قیمت پایه: {base_price:,} تومان",
        ]
        if base_discount:
            text_lines.append(f"🏷️ تخفیف گروهی: {int(base_discount)}٪")
        if extra_discount:
            text_lines.append(f"🏷️ تخفیف ویژه تاریخ خرید: {int(extra_discount)}٪")
        if total_discount:
            text_lines.append(f"🏷️ مجموع تخفیف: {int(total_discount)}٪")
        if start_str and expire_str:
            text_lines.append(f"📅 شروع اعتبار: {start_str}")
            text_lines.append(f"⏳ پایان اعتبار: {expire_str}")
        text_lines.append(
            f"💵 مبلغ نهایی: {final_price:,} تومان\n\n"
            "لطفاً رسید زیر را بررسی و تأیید یا رد کنید:"
        )
        full_text = "\n".join(text_lines)
        kb = order_action_keyboard(order_id)

        receipt_type = getattr(order, "receipt_type", None)
        receipt_value = getattr(order, "receipt_value", None) or getattr(order, "receipt_file_id", None)
        saved_file = None
        if (not receipt_type) or (not receipt_value):
            local = _find_local_receipt(order)
            if local:
                receipt_type = receipt_type or local.get("receipt_type")
                receipt_value = receipt_value or local.get("receipt_value")
                saved_file = local.get("saved_file")

        message_is_original = _is_message_original_receipt(getattr(cb, "message", None), order)

        # ---------- مسیر: اگر پیام فعلی همان پیامِ رسیدِ اصلی باشد -> تلاش برای edit ----------
        if message_is_original and getattr(cb, "message", None):
            if receipt_type in ("file", "photo") and _is_valid_telegram_photo(receipt_value):
                try:
                    await cb.message.edit_photo(
                        photo=receipt_value,
                        caption=full_text,
                        reply_markup=kb,
                        parse_mode="HTML"
                    )
                    await cb.answer()
                    return
                except Exception as e:
                    print(f"⚠️ edit_photo failed (will fallback to send new): {e}\n{traceback.format_exc()}")

            if receipt_type == "url" and receipt_value:
                try:
                    await cb.message.edit_text(f"{full_text}\n\n🧾 لینک رسید: {receipt_value}", reply_markup=kb, parse_mode="HTML")
                    await cb.answer()
                    return
                except Exception as e:
                    print(f"⚠️ edit_text(url) failed (will fallback to send new): {e}\n{traceback.format_exc()}")

        # ---------- مسیر: ارسال پیام جدید ----------
        if receipt_value:
            try:
                await cb.bot.send_photo(
                    chat_id=cb.from_user.id,
                    photo=receipt_value,
                    caption=full_text,
                    reply_markup=kb,
                    parse_mode="HTML",
                )
                await cb.answer()
                return
            except Exception as e:
                print(f"⚠️ send_photo(receipt_value) failed (pending): {e}\n{traceback.format_exc()}")
                if saved_file:
                    try:
                        p = Path(saved_file)
                        if p.exists():
                            await cb.bot.send_photo(
                                chat_id=cb.from_user.id,
                                photo=InputFile(p),
                                caption=full_text,
                                reply_markup=kb,
                                parse_mode="HTML",
                            )
                            await cb.answer()
                            return
                    except Exception as e2:
                        print(f"⚠️ fallback send_photo(InputFile) failed (pending): {e2}\n{traceback.format_exc()}")

            try:
                await cb.bot.send_message(
                    chat_id=cb.from_user.id,
                    text=f"{full_text}\n\nرسید کاربر:\n{receipt_value}",
                    reply_markup=kb,
                    parse_mode="HTML",
                )
                await cb.answer()
                return
            except Exception as e:
                print(f"⚠️ send_message(text receipt) failed (pending): {e}\n{traceback.format_exc()}")

        try:
            await cb.bot.send_message(
                chat_id=cb.from_user.id,
                text=full_text + "\n\n⚠️ رسید برای این سفارش ثبت نشده است.",
                reply_markup=kb,
                parse_mode="HTML",
            )
            await cb.answer()
            return
        except Exception as e:
            print(f"⚠️ final fallback send_message failed (pending): {e}\n{traceback.format_exc()}")
            await cb.answer("⚠️ خطا در نمایش سفارش.", show_alert=True)
            return

    # ---------- مسیر: اگر order پرداخت شده ولی لینک اختصاص نیافته ----------
    if not getattr(order, "assigned_link", None):
        kb = no_link_order_actions_keyboard(order_id)
        no_link_text = (
            f"🔍 سفارش #{order_id} پردازش شده ولی لینک هنوز آماده نیست.\n"
            f"👤 نام کاربر: {user_name_val}\n"
            f"🌐 یوزرنیم: {username_val}\n"
            f"🔢 آیدی عددی: <code>{user_id_str}</code>"
        )
        try:
            edited = False
            if getattr(cb, "message", None):
                if _is_message_original_receipt(cb.message, order):
                    try:
                        await cb.message.edit_text(
                            no_link_text,
                            reply_markup=kb,
                            parse_mode="HTML",
                        )
                        edited = True
                    except Exception:
                        edited = False
                else:
                    edited = False

            if not edited:
                await cb.bot.send_message(
                    chat_id=cb.from_user.id,
                    text=no_link_text,
                    reply_markup=kb,
                    parse_mode="HTML",
                )
            await cb.answer()
            return
        except Exception as e:
            print(f"⚠️ خطا در نمایش وضعیت اختصاص لینک برای order {order_id}: {e}\n{traceback.format_exc()}")
            await cb.answer("⚠️ خطا در نمایش سفارش.", show_alert=True)
            return

    # ---------- مسیر پیش‌فرض ----------
    try:
        base_text = (
            f"🧾 رسید سفارش #{getattr(order, 'order_code', getattr(order, 'id', '-'))}\n"
            f"👤 نام کاربر: {user_name_val}\n"
            f"🌐 یوزرنیم: {username_val}\n"
            f"🔢 آیدی عددی: <code>{user_id_str}</code>\n"
            f"📦 پلن: {plan_name_val}\n"
            f"👥 تعداد دستگاه: {getattr(order, 'device_count', 1)}\n"
            f"💰 قیمت پایه: {getattr(order, 'price', 0):,} تومان\n"
            f"📅 شروع اعتبار: {start_str}\n"
            f"⏳ پایان اعتبار: {expire_str}\n"
            f"💵 مبلغ نهایی: {getattr(order, 'final_price', 0):,} تومان\n\n"
            "لطفاً رسید زیر را بررسی و تأیید یا رد کنید:"
        )

        r_type = getattr(order, "receipt_type", None)
        r_value = getattr(order, "receipt_value", None) or getattr(order, "receipt_file_id", None)
        saved_file = None
        if (not r_type) or (not r_value):
            local = _find_local_receipt(order)
            if local:
                r_type = r_type or local.get("receipt_type")
                r_value = r_value or local.get("receipt_value")
                saved_file = local.get("saved_file")

        message_is_original = _is_message_original_receipt(getattr(cb, "message", None), order)

        if message_is_original and r_type == "photo" and _is_valid_telegram_photo(r_value):
            try:
                await cb.message.edit_photo(
                    photo=r_value,
                    caption=base_text,
                    reply_markup=order_action_keyboard(order.id),
                    parse_mode="HTML"
                )
                await cb.answer()
                return
            except Exception as e:
                print(f"⚠️ edit_photo failed in default path (will fallback to send): {e}\n{traceback.format_exc()}")

        if _is_valid_telegram_photo(r_value):
            try:
                await cb.bot.send_photo(
                    chat_id=cb.from_user.id,
                    photo=r_value,
                    caption=base_text,
                    reply_markup=order_action_keyboard(order.id),
                    parse_mode="HTML",
                )
                await cb.answer()
                return
            except Exception:
                if saved_file:
                    try:
                        p = Path(saved_file)
                        if p.exists():
                            await cb.bot.send_photo(
                                chat_id=cb.from_user.id,
                                photo=InputFile(p),
                                caption=base_text,
                                reply_markup=order_action_keyboard(order.id),
                                parse_mode="HTML",
                            )
                            await cb.answer()
                            return
                    except Exception as e2:
                        print(f"⚠️ fallback InputFile send failed: {e2}\n{traceback.format_exc()}")

            try:
                await cb.bot.send_message(
                    chat_id=cb.from_user.id,
                    text=f"{base_text}\n\nرسید کاربر:\n{r_value}",
                    reply_markup=order_action_keyboard(order.id),
                    parse_mode="HTML",
                )
                await cb.answer()
                return
            except Exception as e3:
                print(f"⚠️ final fallback send_message failed for order {order.id}: {e3}\n{traceback.format_exc()}")

        await cb.bot.send_message(
            chat_id=cb.from_user.id,
            text=base_text + "\n\n⚠️ رسید برای این سفارش ثبت نشده است.",
            reply_markup=order_action_keyboard(order.id),
            parse_mode="HTML",
        )
        await cb.answer()
        return

    except Exception as e:
        print(f"⚠️ خطا در admin_order_detail_merged برای order {order_id}: {e}\n{traceback.format_exc()}")
        await cb.answer("⚠️ خطای داخلی.", show_alert=True)
        return

    
# ───────────── تأیید سفارش توسط ادمین ─────────────
@router.callback_query(F.data.startswith("admin:order_approve:"))
async def order_approve(cb: CallbackQuery):
    order_id = int(cb.data.split(":")[2])

    try:
        await cb.message.edit_reply_markup(None)
    except TelegramBadRequest:
        pass

    await cb.answer("سفارش تأیید شد.", show_alert=False)

    # ۱) تغییر وضعیت سفارش
    await set_order_status(order_id, PaymentStatus.approved)

    # ۲) واکشی سفارش و پلن
    order = await get_order_by_id(order_id)
    if not order:
        logger.error("order_approve: order not found: %s", order_id)
        return
    
    
    # ثبت نهایی مصرف کد تخفیف فقط بعد از پرداخت موفق
    if getattr(order, "discount_code", None):
        discount = await get_discount_code(order.discount_code)

        if discount:
            await use_discount_code(order.discount_code)

            await log_discount_usage(
                user_id=order.user_id,
                discount_id=discount.id,
                order_id=order.id
            )
    
    
    
    
    

    plan = await get_plan_by_id(order.plan_id)

    old_order_info = None
    old_order_id = getattr(order, "renew_order_id", None)

    # ۳) اگر تمدید بوده، اطلاعات سفارش قبلی را آماده کن و به ادمین ارسال کن
    if old_order_id:
        try:
            old_order = await get_order_by_id(old_order_id)
            if old_order:
                prev_plan = await get_plan_by_id(old_order.plan_id)

                # دریافت اطلاعات لینک قبلی
                link_obj = await get_link_for_order(old_order)
                if link_obj:
                    link_name = link_obj.admin_label or "بدون نام"
                    link_url = link_obj.url or "بدون آدرس"
                else:
                    link_name = "ثبت نشده"
                    link_url = getattr(old_order, "assigned_link_url", None) or getattr(old_order, "assigned_link", None) or "ثبت نشده"

                # دریافت اطلاعات کامل کاربر خریدار از تلگرام
                from html import escape
                try:
                    user_chat = await cb.bot.get_chat(order.user_id)
                    user_full_name = escape(user_chat.full_name or "نامشخص")
                    user_username = f"@{user_chat.username}" if user_chat.username else "ندارد"
                except Exception:
                    user_full_name = "نامشخص"
                    user_username = "ندارد"

                admin_msg = (
                    f"ℹ️ سفارش قبلی کاربر برای تمدید:\n"
                    f"👤 نام کاربر: {user_full_name}\n"
                    f"🔗 یوزرنیم: {user_username}\n"
                    f"🆔 آیدی عددی: <code>{order.user_id}</code>\n"
                    f"🆔 Order Code: {old_order.order_code}\n"
                    f"📦 پلن قبلی: {prev_plan.name if prev_plan else '-'}\n"
                    f"🏷 نام لینک قبلی: <code>{link_name}</code>\n"
                    f"🔗 آدرس لینک قبلی: <code>{link_url}</code>"
                )

                for admin_id in ADMINS:
                    try:
                        await cb.bot.send_message(admin_id, admin_msg, parse_mode="HTML")
                    except Exception as e:
                        logger.exception("Failed to send renew notice to admin %s", admin_id)

                # expire/delete سفارش قبلی و لینک آن هنگام تمدید
                try:
                    await delete_expired_order_and_link(old_order_id)
                except Exception:
                    logger.exception("Failed to delete old order and link %s", old_order_id)

        except Exception:
            logger.exception("خطا در پردازش سفارش تمدید")

    # ۴) اختصاص لینک (assigner) — همان کاری که قبلاً داشتی
    try:
        link = await assign_link_for_order(plan.id, order.created_at, order.id)
    except Exception:
        logger.exception("assign_link_for_order failed for order %s", order.id)
        link = None

    link_text = f"🔗 {link}" if link else "⚠️ در حال حاضر لینکی در دسترس نیست.\nاشتراک خریداری‌شده به‌زودی ارسال خواهد شد 🌼."

    # ۵) هدیه ۵٪
    gift_amount = math.floor((order.final_price or 0) * 0.05 / 1000) * 1000
    if gift_amount > 0:
        try:
            await add_gift_credit(order.user_id, gift_amount)
        except Exception:
            logger.exception("Failed to add gift credit for user %s", order.user_id)

    # ۶) پیام نهایی به کاربر
    lines = [
        f"✅ سفارش شما تأیید شد.",
        f"📦 پلن: {plan.name}",
        f"💰 مبلغ پرداختی: {order.final_price:,} تومان"
    ]

    if getattr(order, "discount_percent", None):
        lines.append(f"🟢 تخفیف: {order.discount_percent}%")
    if gift_amount > 0:
        lines.append(f"🎁 هدیه: {gift_amount:,} تومان به کیف پول شما افزوده شد 💝")
    lines.append(link_text)
    if old_order_id:
        lines.append("\n🔄 این سفارش به‌عنوان تمدید ثبت شد و سفارش قبلی حذف و منقضی گردید.")

    try:
        await cb.bot.send_message(order.user_id, "\n".join(lines))
    except Exception:
        logger.exception("خطا در ارسال پیام به کاربر %s", order.user_id)

    # ۷) بروزرسانی لیست معوق‌ها (ارسال مجدد کیبورد)
    try:
        pending = await get_pending_orders()
        if pending:
            kb = await build_pending_orders_keyboard(pending)
            await cb.message.answer("🧾 سفارش‌های معوق:", reply_markup=kb)
        else:
            await cb.message.answer("🧾 هیچ سفارش معوقی نیست.", reply_markup=admin_main_menu())
    except Exception:
        logger.exception("Failed to refresh pending orders keyboard")


# ─────────── رد سفارش توسط ادمین ───────────
@router.callback_query(F.data.startswith("admin:order_reject:"))
async def order_start_reject(cb: CallbackQuery, state: FSMContext):
    order_id = int(cb.data.split(":")[2])
    await state.update_data(order_id=order_id, admin_message_id=cb.message.message_id)

    cancel_kb = InlineKeyboardMarkup(
        inline_keyboard=[[InlineKeyboardButton(text="❌ لغو", callback_data="admin:cancel_reject")]]
    )

    await cb.bot.send_message(cb.from_user.id, "❌ دلیل رد سفارش رو بنویس:", reply_markup=cancel_kb)
    await state.set_state(RejectOrder.waiting_for_reason)
    await cb.answer()


@router.callback_query(F.data == "admin:cancel_reject")
async def order_cancel_reject(cb: CallbackQuery, state: FSMContext):
    await state.clear()
    await cb.message.edit_text("🚫 عملیات رد سفارش لغو شد.", reply_markup=admin_main_menu())
    await cb.answer()


@router.message(RejectOrder.waiting_for_reason)
async def order_receive_reject_reason(msg: types.Message, state: FSMContext):
    data = await state.get_data()
    order_id = data["order_id"]

    await set_order_status(order_id, PaymentStatus.rejected, admin_note=msg.text.strip())
    order = await get_order_by_id(order_id)
    plan = await get_plan_by_id(order.plan_id)

    await msg.bot.send_message(
        order.user_id,
        (
            f"❌ سفارش {order.order_code} برای «{plan.name}» رد شد.\n"
            f"دلیل: {msg.text.strip()}\n\n"
            "اگه رسید دیگه‌ای داری، از دکمه زیر بفرست."
        ),
        reply_markup=build_receipt_keyboard(order.id)
    )

    try:
        admin_msg_id = data.get("admin_message_id")
        if admin_msg_id:
            await msg.bot.edit_message_reply_markup(
                chat_id=msg.chat.id,
                message_id=admin_msg_id,
                reply_markup=None
            )
    except Exception as e:
        print(f"[order_receive_reject_reason] failed to remove markup: {e}")

    await msg.answer("✅ سفارش با موفقیت رد شد.", reply_markup=admin_main_menu())
    await state.clear()




import asyncio
from aiogram import Bot
from aiogram.exceptions import TelegramRetryAfter

async def send_bulk_messages(
    bot: Bot,
    user_ids: list[int],
    text: str,
    *,
    parse_mode: str | None = None,
    delay: float = 0.05,   # حدود 20 پیام در ثانیه (امن)
):
    sent = 0

    for uid in user_ids:
        try:
            await bot.send_message(
                uid,
                text,
                parse_mode=parse_mode
            )
            sent += 1
            await asyncio.sleep(delay)

        except TelegramRetryAfter as e:
            # اگر تلگرام گفت صبر کن
            await asyncio.sleep(e.retry_after + 1)

        except Exception:
            # بلاک، حذف اکانت، ...
            continue

    return sent



# — Link Pool (list/edit/delete) —
@router.callback_query(F.data == "admin:link:menu")
async def link_pool_main(cb: types.CallbackQuery):
    await cb.message.edit_text("🔗 مدیریت لینک‌ها:", reply_markup=link_pool_menu())
    await cb.answer()






# در handlers/admin.py
# ─── نمایش دسته‌بندی‌ها ───
@router.callback_query(F.data == "admin:link:list")
async def link_pool_list(cb: CallbackQuery):
    # ۱) ابتدا دسته‌ها را بگیر
    cats = await get_all_categories()
    if not cats:
        # اگر دسته‌ای نیست، fallback به منوی خالی لینک‌ها
        await cb.message.edit_text(
            "ℹ️ هیچ دسته‌ای برای لینک وجود ندارد.",
            reply_markup=back_to_link_pool_menu()
        )
    else:
        # نمایش کیبورد دسته‌ها
        await cb.message.edit_text(
            "📂 دسته‌بندی‌های لینک‌ها:",
            reply_markup=link_category_keyboard(cats)
        )
    await cb.answer()


from math import ceil
from aiogram.exceptions import TelegramBadRequest
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import InlineKeyboardButton, CallbackQuery

@router.callback_query(F.data.startswith("admin:link:cat:"))
async def link_pool_list_by_category(cb: CallbackQuery):
    try:
        parts = cb.data.split(":")
        cat_id = int(parts[3])
        page = int(parts[4]) if len(parts) > 4 else 1
        page_size = 5  # نمایش ۵ لینک در هر صفحه (به همراه ۳ دکمه اختصاصی برای هر لینک)

        # ۱) دریافت اطلاعات دسته‌بندی و لینک‌ها
        cat_obj = await get_category_by_id(cat_id)
        links = await get_links_by_category(cat_id)

        builder = InlineKeyboardBuilder()

        # ۲) اگر دسته‌بندی لینکی نداشت
        if not links:
            text = f"ℹ️ دسته‌بندی <b>{cat_obj.title if cat_obj else '—'}</b> هنوز لینکی ندارد."
            builder.row(
                InlineKeyboardButton(text="➕ افزودن لینک", callback_data="admin:link:add"),
                InlineKeyboardButton(text="🔙 بازگشت به دسته‌ها", callback_data="admin:link:list"),
                InlineKeyboardButton(text="🔙 بازگشت به منوی لینک‌ها", callback_data="admin:link:menu"),
            )
            try:
                await cb.message.edit_text(text, reply_markup=builder.as_markup(), parse_mode="HTML")
            except TelegramBadRequest as e:
                if "message is not modified" not in str(e):
                    raise e
            await cb.answer()
            return

        # ۳) محاسبه صفحه‌بندی
        total_links = len(links)
        total_pages = max(1, ceil(total_links / page_size))
        page = min(max(1, page), total_pages)

        start_idx = (page - 1) * page_size
        end_idx = start_idx + page_size
        current_links = links[start_idx:end_idx]

        # ۴) ساخت متن اصلی مطابق کد اولیه شما
        cat_title_main = cat_obj.title if cat_obj else "—"
        text = f"📋 <b>لینک‌های دسته {cat_title_main}</b> (صفحه {page} از {total_pages}):\n\n"

        for lk in current_links:
            name = lk.admin_label or lk.url
            plan = await get_plan_by_id(lk.plan_id) if lk.plan_id else None
            plan_name = plan.name if plan else "—"

            if plan and getattr(plan, "category_id", None):
                cat = await get_category_by_id(plan.category_id)
                cat_title = cat.title if cat else cat_title_main
            else:
                cat_title = cat_title_main

            text += (
                f"• لینک #{lk.id}: {name}\n"
                f"  🔗 آدرس: <code>{lk.url}</code>\n"
                f"  📦 پلن: {plan_name}\n"
                f"  🗂 دسته: {cat_title}\n"
                f"  ⚙️ ظرفیت: {lk.used_users}/{lk.capacity}   وضعیت: {lk.status}\n\n"
            )

        # ۵) ساخت ۳ دکمه اختصاصی (ویرایش، حذف، پیام) برای هر لینک
        for lk in current_links:
            btn_edit = InlineKeyboardButton(
                text=f"✏️ ویرایش {lk.id}",
                callback_data=f"admin:link:edit:{lk.id}"
            )
            btn_del = InlineKeyboardButton(
                text=f"🗑️ حذف {lk.id}",
                callback_data=f"admin:link:del:{lk.id}"
            )
            btn_msg = InlineKeyboardButton(
                text="📨 پیام به مشترکین",
                callback_data=f"admin:link:message:{lk.id}"
            )
            builder.row(btn_edit, btn_del, btn_msg)

        # ۶) اضافه کردن دکمه‌های ناوبری صفحه‌بندی (قبلی / بعدی)
        if total_pages > 1:
            pag_row = []
            if page > 1:
                pag_row.append(InlineKeyboardButton(text="⬅️ قبلی", callback_data=f"admin:link:cat:{cat_id}:{page-1}"))
            pag_row.append(InlineKeyboardButton(text=f"صفحه {page}/{total_pages}", callback_data=f"admin:link:cat:{cat_id}:{page}"))
            if page < total_pages:
                pag_row.append(InlineKeyboardButton(text="بعدی ➡️", callback_data=f"admin:link:cat:{cat_id}:{page+1}"))
            builder.row(*pag_row)

        # ۷) دکمه‌های عمومی پایین
        builder.row(
            InlineKeyboardButton(text="➕ افزودن لینک", callback_data="admin:link:add"),
            InlineKeyboardButton(text="🔙 بازگشت به دسته‌ها", callback_data="admin:link:list"),
            InlineKeyboardButton(text="🔙 بازگشت به منوی لینک‌ها", callback_data="admin:link:menu"),
        )

        try:
            await cb.message.edit_text(text, reply_markup=builder.as_markup(), parse_mode="HTML")
        except TelegramBadRequest as e:
            if "message is not modified" not in str(e):
                raise e

        await cb.answer()

    except Exception as e:
        import traceback
        print(f"❌ Error in link_pool_list_by_category: {e}")
        traceback.print_exc()
        await cb.answer("⚠️ خطا در بارگذاری لینک‌ها.", show_alert=True)




# --------- ارسال یام به مشترکین لینک -----
@router.callback_query(F.data.startswith("admin:link:message:"))
async def link_message_start(cb: types.CallbackQuery, state: FSMContext):
    link_id = int(cb.data.split(":")[-1])
    await state.update_data(link_id=link_id)
    await cb.message.answer("📝 لطفاً متن پیام را برای مشترکین ارسال کنید:")
    await state.set_state(SendLinkMessage.waiting_for_message)
    await cb.answer()

@router.message(SendLinkMessage.waiting_for_message)
async def link_message_send(msg: types.Message, state: FSMContext):
    data = await state.get_data()
    link_id = data["link_id"]
    user_ids = await get_subscribers_by_link_id(link_id)

    if not user_ids:
        await msg.answer("⚠️ هیچ مشترکی برای این لینک وجود ندارد.")
    else:
        text = (
            "📩 یک پیام از سمت مدیریت برای شما ارسال شده:\n\n"
            f"{msg.text}"
        )

        sent = await send_bulk_messages(
            bot=bot,
            user_ids=user_ids,
            text=text
        )

        await msg.answer(f"✅ پیام به {sent} کاربر ارسال شد.")


    await state.clear()






@router.callback_query(F.data.startswith("admin:link:del:"))
async def link_pool_delete(cb: types.CallbackQuery):
    await delete_link_by_id(int(cb.data.split(":")[3]))
    await cb.answer("✅ حذف شد.")
    return await link_pool_list(cb)



# 1) شروعِ ویرایش: دکمه «✏️ ویرایش لینک» کلیک شده
@router.callback_query(F.data.startswith("admin:link:edit:"))
async def link_pool_edit_start(cb: types.CallbackQuery, state: FSMContext):
    # استخراج link_id
    link_id = int(cb.data.split(":")[-1])
    await state.update_data(link_id=link_id)

    # ساخت کیبورد انتخاب فیلد
    kb = InlineKeyboardMarkup(inline_keyboard=[
        [InlineKeyboardButton(text="ویرایش آدرس",       callback_data="admin:link:field:url")],
        [InlineKeyboardButton(text="ویرایش ظرفیت",     callback_data="admin:link:field:capacity")],
        [InlineKeyboardButton(text="ویرایش نام داخلی", callback_data="admin:link:field:label")],
        [InlineKeyboardButton(text="ویرایش دسته‌بندی", callback_data="admin:link:field:category")],
        [InlineKeyboardButton(text="🔙 بازگشت",         callback_data="admin:link:list")],
    ])
    await cb.message.edit_text("کدوم فیلد لینک رو می‌خوای ویرایش کنی؟", reply_markup=kb)
    await state.set_state(EditLink.waiting_for_field)
    await cb.answer()


# 2a) انتخاب فیلد «آدرس/ظرفیت/نام داخلی»
@router.callback_query(EditLink.waiting_for_field, F.data.in_({"admin:link:field:url", "admin:link:field:capacity", "admin:link:field:label"}))
async def link_pool_edit_field_value(cb: types.CallbackQuery, state: FSMContext):
    # ذخیرهٔ نوع فیلد
    field_map = {
        "admin:link:field:url":      "url",
        "admin:link:field:capacity": "capacity",
        "admin:link:field:label":    "admin_label",
    }
    selected = cb.data
    await state.update_data(field=field_map[selected])

    # رفتن به مرحلهٔ دریافت مقدار
    await state.set_state(EditLink.waiting_for_value)
    await cb.message.edit_text("📤 لطفاً مقدار جدید را ارسال کنید:")
    await cb.answer()


# 2b) انتخاب فیلد «دسته‌بندی»
@router.callback_query(EditLink.waiting_for_field, F.data == "admin:link:field:category")
async def link_pool_edit_field_category(cb: types.CallbackQuery, state: FSMContext):
    cats = await get_all_categories()
    kb = InlineKeyboardMarkup(
        inline_keyboard=[
            [
                InlineKeyboardButton(
                    text=c.title,
                    callback_data=f"admin:link:cat_for_plan:{c.id}"
                )
            ] for c in cats
        ] + [[InlineKeyboardButton(text="🔙 برگرد", callback_data="admin:link:list")]]
    )
    await cb.message.edit_text("ابتدا یک دسته انتخاب کن تا پلن‌هاشو ببینی:", reply_markup=kb)
    await state.set_state(EditLink.waiting_for_category_selection)
    await cb.answer()


# 3a) پس از انتخاب دسته، نمایش پلن‌ها
@router.callback_query(EditLink.waiting_for_category_selection, F.data.startswith("admin:link:cat_for_plan:"))
async def link_pool_show_plans(cb: types.CallbackQuery, state: FSMContext):
    cat_id = int(cb.data.split(":")[-1])
    await state.update_data(field="plan_id", category_id=cat_id)
    plans = await get_plans_by_category(cat_id)
    kb = InlineKeyboardMarkup(
        inline_keyboard=[
            [
                InlineKeyboardButton(
                    text=p.name,
                    callback_data=f"admin:link:newplan:{p.id}"
                )
            ] for p in plans
        ] + [[InlineKeyboardButton(text="🔙 برگرد", callback_data="admin:link:list")]]
    )
    await cb.message.edit_text("حالا یک پلن از این دسته انتخاب کن:", reply_markup=kb)
    await state.set_state(EditLink.waiting_for_plan_selection)
    await cb.answer()


# 3b) پس از انتخاب پلن، ذخیره و پایان ویرایش
@router.callback_query(EditLink.waiting_for_plan_selection, F.data.startswith("admin:link:newplan:"))
async def link_pool_update_plan(cb: types.CallbackQuery, state: FSMContext):
    data = await state.get_data()
    link_id     = data["link_id"]
    new_plan_id = int(cb.data.split(":")[-1])
    await update_link_field(link_id, "plan_id", new_plan_id)
    await state.clear()
    # فرض: link_pool_menu() کیبورد منوی لینک‌ها را می‌سازد
    await cb.message.edit_text("✅ پلن لینک با موفقیت تغییر کرد.", reply_markup=link_pool_menu())
    await cb.answer()


# 4) دریافت مقدار جدید برای url/capacity/label و ذخیره
@router.message(EditLink.waiting_for_value)
async def link_pool_edit_value(msg: types.Message, state: FSMContext):
    data    = await state.get_data()
    link_id = data["link_id"]
    field   = data["field"]
    raw     = msg.text.strip()

    # اعتبارسنجی ظرفیت
    if field == "capacity":
        if not raw.isdigit():
            return await msg.answer("⛔️ فقط عدد وارد کن.")
        value = int(raw)
        await update_link_field(link_id, field, value)
        await msg.answer(f"✅ ظرفیت لینک با موفقیت به {value} تغییر کرد.")

    elif field in ("url", "admin_label"):
        value = raw

        if field == "url":
            from html import escape
            from database.queries import update_link_for_all_orders, get_subscribers_by_link_id

            # آپدیت لینک‌ها و سفارش‌ها
            await update_link_for_all_orders(link_id, value)

            # اطلاع‌رسانی به کاربران
            user_ids = await get_subscribers_by_link_id(link_id)

            safe_url = escape(value)
            text = (
                "🔄 لینک اشتراک شما به‌روز شد.\n"
                "حتماً لینک جدید را جایگزین لینک قبلی کنید:\n\n"
                f"<code>{safe_url}</code>"
            )

            sent = await send_bulk_messages(
                bot=bot,
                user_ids=user_ids,
                text=text,
                parse_mode="HTML"
            )

            await msg.answer(f"✅ لینک به‌روزرسانی شد و به {sent} کاربر اطلاع داده شد.")

        else:
            # فقط admin_label
            await update_link_field(link_id, field, value)
            await msg.answer("✅ نام داخلی لینک ویرایش شد.")

    await state.clear()



# — Add Link (with category & plan) —
@router.callback_query(F.data == "admin:link:add")
async def link_pool_add(cb: types.CallbackQuery, state: FSMContext):
    await cb.message.edit_text("📤 لینک جدید رو بفرست:")
    await state.set_state(AddLink.waiting_for_url)
    await cb.answer()

@router.message(AddLink.waiting_for_url)
async def receive_link_url(msg: types.Message, state: FSMContext):
    await state.update_data(url=msg.text.strip())
    await msg.answer("🔢 ظرفیت لینک:")
    await state.set_state(AddLink.waiting_for_capacity)

@router.message(AddLink.waiting_for_capacity)
async def receive_link_capacity(msg: types.Message, state: FSMContext):
    if not msg.text.isdigit():
        return await msg.answer("⛔️ فقط عدد.")
    await state.update_data(capacity=int(msg.text))

    # ⬇️ نمایش کیبورد نوع لینک
    from bot.keyboards.admin import link_type_keyboard
    await msg.answer("📂 نوع لینک را انتخاب کن:", reply_markup=link_type_keyboard())
    await state.set_state(AddLink.waiting_for_type)


    @router.callback_query(
        StateFilter(AddLink.waiting_for_type),
        F.data.startswith("admin:link:type:")
    )
    async def link_select_type(cb: types.CallbackQuery, state: FSMContext):
        link_type = cb.data.split(":")[-1]
        await state.update_data(link_type=link_type)

        cats = await get_all_categories()
        # کیبورد اختصاصی برای «افزودن لینک» با prefix جدید
        buttons = [
            InlineKeyboardButton(
                text=c.title,
                callback_data=f"admin:link:add:cat:{c.id}"
            )
            for c in cats
        ]
        # ایجاد یک Builder
        builder = InlineKeyboardBuilder()
        # اضافه کردن دکمه‌ها در دو ستون (width=2)
        builder.row(*buttons, width=2)
        # ساختن InlineKeyboardMarkup نهایی
        kb = builder.as_markup()

        await cb.message.edit_text("📦 یک دسته‌بندی برای لینک انتخاب کن:", reply_markup=kb)

        await state.set_state(AddLink.waiting_for_cat_link)
        await cb.answer()





@router.callback_query(
    StateFilter(AddLink.waiting_for_cat_link),
    # حالا فقط کال‌بک‌های با prefix جدید را می‌گیرد
    F.data.startswith("admin:link:add:cat:")
)

async def link_select_category(cb: types.CallbackQuery, state: FSMContext):
    await state.update_data(category_id=int(cb.data.split(":")[-1]))
    plans = await get_plans_by_category((await state.get_data())["category_id"])
    kb = InlineKeyboardMarkup(
        inline_keyboard=[
            [
                InlineKeyboardButton(text=p.name, callback_data=f"admin:link:plan:{p.id}")
            ] for p in plans
        ]
    )
    await cb.message.edit_text("📋 حالا یک پلن انتخاب کن:", reply_markup=kb)
    await state.set_state(AddLink.waiting_for_plan)
    await cb.answer()

@router.callback_query(
    StateFilter(AddLink.waiting_for_plan),
    F.data.startswith("admin:link:plan:")
)
async def link_select_plan(cb: types.CallbackQuery, state: FSMContext):
    await state.update_data(plan_id=int(cb.data.split(":")[-1]))
    await cb.message.edit_text("🖋 نام داخلی لینک رو وارد کن:", reply_markup=None)
    await state.set_state(AddLink.waiting_for_label)
    await cb.answer()

@router.message(AddLink.waiting_for_label)
async def receive_link_label(msg: types.Message, state: FSMContext):
    data = await state.get_data()
    await add_link_to_pool(
        url=data["url"],
        capacity=data["capacity"],
        link_type=LinkType(data["link_type"]),  # ← این خط اضافه شده
        admin_label=msg.text.strip(),
        plan_id=data["plan_id"]
    )
    await state.clear()
    await msg.answer("✅ لینک جدید اضافه شد.", reply_markup=admin_main_menu())

# --------- اختصاص لینک ---------
from aiogram.exceptions import TelegramBadRequest

@router.callback_query(F.data.startswith("admin:assign_link_select:"))
async def select_link_for_order(cb: types.CallbackQuery):
    order_id = int(cb.data.split(":")[-1])

    # 1) گرفتن اطلاعات سفارش
    order = await get_order_by_id(order_id)
    if not order:
        return await cb.answer("⚠️ سفارش پیدا نشد.", show_alert=True)

    # 2) استخراج پلن و سپس دسته‌بندی آن
    plan = await get_plan_by_id(order.plan_id)
    cat_id = plan.category_id

    # 3) گرفتن فقط لینک‌های همان دسته
    links = await get_links_by_category(cat_id)

    # 4) فیلتر ظرفیت و وضعیت
    available_links = [
        lk for lk in links
        if lk.capacity > lk.used_users and lk.status == "active"
    ]

    if not available_links:
        return await cb.answer(
            "❌ هیچ لینک فعالی در این دسته با ظرفیت خالی یافت نشد.",
            show_alert=True
        )

    # 5) ساخت متن و کیبورد
    text = "🔗 یکی از لینک‌های مرتبط با دسته‌بندی سفارش را انتخاب کن:"
    kb = InlineKeyboardBuilder()
    for lk in available_links:
        kb.button(
            text=f"{lk.admin_label or lk.url} ({lk.used_users}/{lk.capacity})",
            callback_data=f"admin:assign_link_confirm:{order_id}:{lk.id}"
        )
    # دکمه‌ی برگرد
    kb.button(
        text="🔙 برگرد",
        callback_data="admin:assign_link:back"
    )

    kb.adjust(1)
    markup = kb.as_markup()

    # ۶) تلاش برای ویرایش متن و کیبورد
    try:
        await cb.message.edit_text(text, reply_markup=markup)
    except TelegramBadRequest as e:
        # اگر خطای «message is not modified» پیش اومد، نادیده بگیر
        if "message is not modified" not in str(e):
            raise

    # ۷) جواب به callback تا loading حذف بشه
    await cb.answer()


@router.callback_query(F.data == "admin:assign_link:back")
async def assign_link_back(cb: CallbackQuery):
    await cb.message.edit_text("🔙 برگشتی به مرحله قبل.", reply_markup=admin_main_menu())
    await cb.answer()




@router.callback_query(F.data.startswith("admin:assign_link_confirm:"))
async def confirm_manual_link_assignment(cb: types.CallbackQuery):
    # ۱) استخراج order_id و link_id
    _, _, order_id_str, link_id_str = cb.data.split(":")
    order_id, link_id = int(order_id_str), int(link_id_str)

    # ۲) واکشی سفارش
    order = await get_order_by_id(order_id)
    if not order:
        return await cb.answer("⚠️ سفارش پیدا نشد.", show_alert=True)

    # ۳) واکشی URL لینک فعلی و URL لینک جدید
    current_url = order.assigned_link       # ممکنه None باشه
    new_url = await get_link_url_by_id(link_id)
    if not new_url:
        return await cb.answer("⚠️ لینک انتخاب‌شده معتبر نیست!", show_alert=True)

    # ۴) چک تکراری بودن
    if current_url == new_url:
        return await cb.answer(
            "⚠️ این همون لینک فعلی سفارش هست! لطفاً یه لینک جدید انتخاب کن.",
            show_alert=True
        )

    # ۵) اختصاص لینک جدید (حالا که فرق داشت)
    link_url = await assign_specific_link_to_order(order_id, link_id)
    if not link_url:
        return await cb.answer("⛔️ خطا: لینک ظرفیت کامله یا معتبر نیست.", show_alert=True)

    # ۶) ارسال لینک به کاربر
    plan = await get_plan_by_id(order.plan_id)
    tutorial_url = "https://t.me/+mR2C6Z5Y8MdlYzE0"
    await cb.bot.send_message(
        order.user_id,
        (
            f"✅ لینک سفارش {order.order_code} برات صادر شد!\n"
            f"پلن: {plan.name}\n\n"
            f"🔗 لینک شما:\n<a href=\"{link_url}\">{link_url}</a>\n"
            f'لینک خود را کپی کنید و برای مشاهده آموزش اتصال <a href="{tutorial_url}">اینجا</a> کلیک کن!'
        ),
        parse_mode=ParseMode.HTML,
        disable_web_page_preview=True,
    )

    # ۷) آپدیت پیام ادمین
    await cb.message.edit_text(f"✅ لینک اختصاص داده شد:\n{link_url}", reply_markup=admin_active_users_kb())
    await cb.answer("لینک اختصاص یافت ✅")


# ------ جستجو لینک -----
@router.callback_query(F.data == "admin:link:search")
async def admin_link_search_start(cb: types.CallbackQuery, state: FSMContext):
    await cb.message.answer("🔍 لطفاً بخشی از نام لینک را وارد کنید:")
    await state.set_state(LinkSearchStates.waiting_for_query)
    await cb.answer()

@router.message(LinkSearchStates.waiting_for_query)
async def admin_link_search_execute(msg: types.Message, state: FSMContext):
    keyword = msg.text.strip()
    # فرض می‌کنیم تابع زیر در queries.py پیاده‌ست:
    # SELECT * FROM links
    # WHERE link_url LIKE '%keyword%' OR admin_note LIKE '%keyword%'
    results = await search_links_by_keyword(keyword)

    if not results:
        await msg.answer("⚠️ لینکی پیدا نشد. مجدداً تلاش کنید یا کل عبارت را وارد کنید.")
    else:
        kb = InlineKeyboardBuilder()
        for link in results:
            label = link.admin_label or link.link_url
            kb.button(text=label, callback_data=f"admin:link:select:{link.id}")
        kb.button(text="🔙 بازگشت", callback_data="admin:link:menu")
        kb.adjust(1)

        await msg.answer(
            f"✅ {len(results)} لینک پیدا شد. برای انتخاب، روی لینک کلیک کنید:",
            reply_markup=kb.as_markup()
        )

    await state.clear()



# این تابع کاملاً مشابه link_pool_edit_start است،
# ولی با prefix جدید callback_data="admin:link:select:<id>"
from database.queries import get_link_by_id

# ─────────── مدیریت لینک انتخاب‌شده از جستجو ───────────
@router.callback_query(F.data.startswith("admin:link:select:"))
async def admin_link_search_select(cb: types.CallbackQuery, state: FSMContext):
    link_id = int(cb.data.split(":")[-1])
    await state.update_data(link_id=link_id)

    # دریافت اطلاعات لینک جهت نمایش جزئیات
    link = await get_link_by_id(link_id)
    if not link:
        await cb.answer("❌ لینک مورد نظر یافت نشد.", show_alert=True)
        return

    name = link.admin_label or "بدون نام"
    text = (
        f"🔍 <b>مدیریت لینک انتخاب‌شده: #{link.id}</b>\n\n"
        f"• نام داخلی: {name}\n"
        f"• آدرس: <code>{link.url}</code>\n"
        f"• ظرفیت: {link.used_users}/{link.capacity}\n"
        f"• وضعیت: {link.status}\n\n"
        "لطفاً عملیات مورد نظر را انتخاب کنید:"
    )

    kb = InlineKeyboardMarkup(inline_keyboard=[
        [InlineKeyboardButton(text="✏️ ویرایش آدرس",       callback_data="admin:link:field:url")],
        [InlineKeyboardButton(text="✏️ ویرایش ظرفیت",     callback_data="admin:link:field:capacity")],
        [InlineKeyboardButton(text="✏️ ویرایش نام داخلی", callback_data="admin:link:field:label")],
        [InlineKeyboardButton(text="✏️ ویرایش دسته‌بندی", callback_data="admin:link:field:category")],
        [InlineKeyboardButton(text="🗑️ حذف این لینک",     callback_data=f"admin:link:del:{link_id}")],
        [InlineKeyboardButton(text="📨 پیام به مشترکین", callback_data=f"admin:link:message:{link_id}")],
        [InlineKeyboardButton(text="🔙 بازگشت به منوی لینک‌ها", callback_data="admin:link:menu")],
    ])

    await cb.message.edit_text(text, reply_markup=kb, parse_mode="HTML")
    await state.set_state(EditLink.waiting_for_field)
    await cb.answer()



# ریست کردن دیتابیس 

# 1. مرحله‌ی اول: درخواست تأیید


@router.callback_query(F.data == "admin:reset_db")
async def ask_reset_db(cb: CallbackQuery):
    if not is_admin(cb.from_user.id):
        return await cb.answer("⛔️ دسترسی ندارید.", show_alert=True)

    kb = InlineKeyboardMarkup(inline_keyboard=[
        [
            InlineKeyboardButton(text="❌ انصراف", callback_data="admin:reset_db_cancel"),
            InlineKeyboardButton(text="⚠️ تأیید ریست", callback_data="admin:reset_db_confirm"),
        ]
    ])
    await cb.message.answer(
        "⚠️ آیا مطمئن هستید که می‌خواهید دیتابیس را **کاملاً** ریست کنید؟\n"
        "این کار باعث حذف همه‌ی داده‌ها می‌شود و غیرقابل بازگشت است!",
        reply_markup=kb,
        parse_mode="Markdown"
    )
    await cb.answer()


# bot/handlers/admin.py

@router.callback_query(F.data == "admin:reset_db_cancel")
async def cancel_reset_db(cb: CallbackQuery):
    await cb.answer("❌ عملیات ریست دیتابیس لغو شد.", show_alert=True)
    # بازگشت به منوی اصلی ادمین
    await cb.message.answer("🛠 پنل ادمین:", reply_markup=admin_main_menu())

# bot/handlers/admin.py

from database.db import engine
from database.models import Base

@router.callback_query(F.data == "admin:reset_db_confirm")
async def do_reset_db(cb: CallbackQuery):
    await cb.answer("⏳ در حال ریست دیتابیس…", show_alert=False)
    # اجرا در کانکشن سینکرون متادیتا
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
        await conn.run_sync(Base.metadata.create_all)

    await cb.message.answer(
        "✅ دیتابیس با موفقیت ریست شد.\n"
        "اکنون اسکیمای خالی بدون هیچ داده‌ای آماده است."
    )
    # بازگشت به منوی اصلی ادمین
    await cb.message.answer("🛠 پنل ادمین:", reply_markup=admin_main_menu())


# گزارش ماهانه فروش 
# کیبورد سادهٔ «بازگشت»
def back_button_keyboard() -> InlineKeyboardMarkup:
    return InlineKeyboardMarkup(
        inline_keyboard=[
            [InlineKeyboardButton(text="🔙 بازگشت", callback_data="admin:back")]
        ]
    )

# ————————— شروع انتخاب گزارش —————————
@router.callback_query(F.data == "admin:monthly_report")
async def monthly_report_choice(cb: types.CallbackQuery):
    if not is_admin(cb.from_user.id):
        return await cb.answer("⛔️ دسترسی ندارید.", show_alert=True)
    await cb.message.edit_text(
        "می‌خواهید گزارش فروش ماهانه را چطور دریافت کنید؟",
        reply_markup=monthly_report_choice_keyboard()
    )
    await cb.answer()

# ————————— نمایش در چت —————————
@router.callback_query(F.data == "admin:monthly_inline")
async def monthly_report_inline(cb: types.CallbackQuery):
    plan_rows     = await get_monthly_plan_sales_report()
    category_rows = await get_monthly_category_sales_report()

    lines = [
        "📊 <b>گزارش فروش ۳۰ روز گذشته</b>", ""
    ]
    # — per plan —
    lines += [
        "<u>📦 بر اساس پلن</u>",
        f"{'پلن':<25}{'تعداد':>7}{'درآمد':>12}",
        "―" * 44
    ]
    total_orders = 0
    total_revenue = 0
    for name, cnt, rev in plan_rows:
        cnt = cnt or 0
        rev = rev or 0
        lines.append(f"{name:<25}{cnt:>7,}{rev:>12,} تومان")
        total_orders  += cnt
        total_revenue += rev

    # — per category —
    lines += [
        "",
        "<u>🗂 بر اساس دسته‌بندی</u>",
        f"{'دسته':<25}{'تعداد':>7}{'درآمد':>12}",
        "―" * 44
    ]
    for title, cnt, rev in category_rows:
        cnt = cnt or 0
        rev = rev or 0
        lines.append(f"{title:<25}{cnt:>7,}{rev:>12,} تومان")

    # — grand totals —
    lines += [
        "",
        f"🔢 <b>مجموع سفارش‌ها:</b> {total_orders:,}",
        f"💰 <b>مجموع درآمد:</b> {total_revenue:,} تومان"
    ]

    await cb.message.edit_text(
        "\n".join(lines),
        parse_mode="HTML",
        reply_markup=back_button_keyboard()
    )
    await cb.answer()

# ————————— خروجی فایل CSV —————————
@router.callback_query(F.data == "admin:monthly_file")
async def monthly_report_file(cb: types.CallbackQuery):
    plan_rows     = await get_monthly_plan_sales_report()
    category_rows = await get_monthly_category_sales_report()

    # ۱) CSV را در StringIO بنویسیم
    buffer = io.StringIO()
    writer = csv.writer(buffer)
    writer.writerow(["پلن", "تعداد", "درآمد"])
    total_orders = total_revenue = 0
    for name, cnt, rev in plan_rows:
        cnt = cnt or 0
        rev = rev or 0
        writer.writerow([name, cnt, rev])
        total_orders  += cnt
        total_revenue += rev

    writer.writerow([])
    writer.writerow(["دسته‌بندی", "تعداد", "درآمد"])
    for title, cnt, rev in category_rows:
        cnt = cnt or 0
        rev = rev or 0
        writer.writerow([title, cnt, rev])

    writer.writerow([])
    writer.writerow(["مجموع سفارش‌ها", total_orders])
    writer.writerow(["مجموع درآمد", total_revenue])

    # ۲) تبدیل به بایت با BOM برای Excel
    csv_bytes = buffer.getvalue().encode("utf-8-sig")
    file = BufferedInputFile(csv_bytes, filename="monthly_report.csv")

    # ۳) ارسال فایل
    await cb.message.answer_document(
        file,
        caption="📄 گزارش فروش ۳۰ روز گذشته (CSV)"
    )
    await cb.answer()

# ————————— توابع گزارش‌گیری —————————
async def get_monthly_plan_sales_report() -> list[tuple[str, int, int]]:
    one_month_ago = datetime.utcnow() - timedelta(days=30)
    async with async_session() as session:
        stmt = (
            select(
                Plan.name,
                func.count(Order.id),
                func.sum(Order.final_price)
            )
            .join(Plan, Plan.id == Order.plan_id)
            .where(
                Order.payment_status == PaymentStatus.approved,
                Order.created_at >= one_month_ago
            )
            .group_by(Plan.name)
        )
        res = await session.execute(stmt)
        return res.all()

async def get_monthly_category_sales_report() -> list[tuple[str, int, int]]:
    one_month_ago = datetime.utcnow() - timedelta(days=30)
    async with async_session() as session:
        stmt = (
            select(
                PlanCategory.title,
                func.count(Order.id),
                func.sum(Order.final_price)
            )
            .join(Plan, Plan.category_id == PlanCategory.id)
            .join(Order, Order.plan_id == Plan.id)
            .where(
                Order.payment_status == PaymentStatus.approved,
                Order.created_at >= one_month_ago
            )
            .group_by(PlanCategory.title)
        )
        res = await session.execute(stmt)
        return res.all()




# لیست کاربران فعال
from aiogram import F, types
from aiogram.types import CallbackQuery
from bot.keyboards.admin import admin_main_menu
from database.db import async_session # یا تابع معادل شما
from datetime import datetime

import jdatetime
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.exceptions import TelegramBadRequest
from database.queries import get_active_orders_page, count_active_orders
from database.db import async_session
from bot.keyboards.admin import admin_main_menu


@router.callback_query(F.data.startswith("admin:list_users"))
async def admin_list_active_users(cb: CallbackQuery):
    print(f"[INFO] admin_list_active_users called; data={cb.data} from={cb.from_user.id}")

    # پاسخ فوری (spinner)
    try:
        await cb.answer()
    except Exception as e:
        print(f"[WARN] cb.answer() failed: {e}")

    # parse page
    try:
        parts = (cb.data or "").split(":")
        page = int(parts[2]) if len(parts) >= 3 and parts[2].isdigit() else 1
    except Exception as e:
        print(f"[ERROR] parse page failed: {e}", e)
        page = 1

    PAGE_SIZE = 10
    offset = (page - 1) * PAGE_SIZE

    try:
        # فراخوانی امن DB
        async with async_session() as session:
            orders = await get_active_orders_page(session, PAGE_SIZE, offset)
            total = await count_active_orders(session)

        print(f"[INFO] fetched orders_count={len(orders)} total={total} page={page}")

        if not orders:
            print("[INFO] no active orders found -> show main menu")
            # اگر پیام موجوده ادیت کن، وگرنه پیام جدید بفرست
            try:
                if getattr(cb, "message", None):
                    await cb.message.edit_text(
                        "❌ کاربری با اشتراک فعال وجود ندارد.",
                        reply_markup=admin_main_menu()
                    )
                else:
                    await cb.bot.send_message(
                        chat_id=cb.from_user.id,
                        text="❌ کاربری با اشتراک فعال وجود ندارد.",
                        reply_markup=admin_main_menu()
                    )
            except Exception as e:
                print(f"[ERROR] sending 'no active orders' message: {e}", exc_info=True)
            return

        # ساخت متن
        lines = [f"📋 کاربران فعال (صفحه {page}):\n"]
        for o in orders:
            user_id = getattr(o, "user_id", "-")
            username = None
            if getattr(o, "user", None):
                username = getattr(o.user, "username", None)

            if username:
                user_label = f"@{username} | ID: {user_id}"
            else:
                user_label = f"ID: {user_id}"

            try:
                jdate = jdatetime.datetime.fromgregorian(datetime=o.expiration_date).strftime("%Y/%m/%d")
            except Exception:
                jdate = str(getattr(o, "expiration_date", "-"))

            lines.append(f"• {user_label} — منقضی: <code>{jdate}</code>")

        text = "\n".join(lines)

        # ساخت کیبورد: ردیف کنترل + ردیف صفحه‌بندی (اگر لازم بود)
        rows = []
        rows.append([
            InlineKeyboardButton(text="🔍 جستجو", callback_data="admin:search_user"),
            InlineKeyboardButton(text="🔙 بازگشت", callback_data="admin:main_menu"),
        ])

        pag_buttons = []
        if page > 1:
            pag_buttons.append(InlineKeyboardButton(text="⬅️ قبلی", callback_data=f"admin:list_users:{page-1}"))
        if offset + PAGE_SIZE < (total or 0):
            pag_buttons.append(InlineKeyboardButton(text="بعدی ➡️", callback_data=f"admin:list_users:{page+1}"))
        if pag_buttons:
            rows.append(pag_buttons)

        kb = InlineKeyboardMarkup(inline_keyboard=rows)

        # تلاش برای ویرایش پیام موجود؛ اگر fail شد -> ارسال پیام جدید
        try:
            if getattr(cb, "message", None):
                try:
                    await cb.message.edit_text(text, reply_markup=kb, parse_mode="HTML")
                    print("[INFO] edited message with user list")
                    return
                except TelegramBadRequest as e:
                    # اگر پیام تغییر نکرده یا خطای تلگرام بود، لاگ و fallback به ارسال پیام جدید
                    err = str(e)
                    print(f"[WARN] edit_text TelegramBadRequest: {err}")
                    # اگر خطای "message is not modified" بود، می‌تونیم آن را نادیده بگیریم یا پیام جدید بفرستیم.
                    # در اینجا همیشه fallback می‌کنیم به ارسال پیام جدید.
            # اگر cb.message نیست یا edit_text موفق نبود، ارسال پیام جدید
            await cb.bot.send_message(chat_id=cb.from_user.id, text=text, reply_markup=kb, parse_mode="HTML")
            print("[INFO] sent new message with user list (fallback)")
            return

        except Exception as e:
            print(f"[ERROR] final send/edit failed: {e}", exc_info=True)
            try:
                await cb.answer("⚠️ خطا در نمایش لیست", show_alert=True)
            except Exception:
                pass
            return

    except Exception as e:
        print(f"[ERROR] admin_list_active_users exception: {e}", exc_info=True)
        try:
            await cb.answer("⚠️ خطا رخ داد.", show_alert=True)
        except Exception:
            pass
        return








from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import InlineKeyboardButton

@router.callback_query(F.data == "admin:search_user")
async def admin_search_user_start(cb: CallbackQuery, state: FSMContext):
    await state.set_state(AdminSearchStates.waiting_for_query)

    # ساخت کیبورد با دکمه انصراف
    builder = InlineKeyboardBuilder()
    builder.button(text="🔙 انصراف", callback_data="admin:cancel_search")
    builder.adjust(1)

    await cb.message.edit_text(
        "🔍 لطفاً نام کاربری (بدون @) یا شناسه عددی کاربر را وارد کنید:",
        reply_markup=builder.as_markup()
    )
    await cb.answer()



from aiogram.types import Message
from database.db import async_session

from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from database.queries import search_users
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from database.db import async_session
from database.queries import get_active_orders, update_order_expiration

@router.message(AdminSearchStates.waiting_for_query)
async def admin_search_user_process(msg: Message, state: FSMContext):
    keyword = msg.text.strip()
    users = await search_users(keyword)

    if not users:
        await msg.answer(
            f"⚠️ هیچ کاربری با عبارت `{keyword}` پیدا نشد.",
            reply_markup=InlineKeyboardMarkup(
                inline_keyboard=[[
                    InlineKeyboardButton(text="🔙 بازگشت", callback_data="admin:list_users")
                ]]
            )
        )
        await state.clear()
        return

    lines = ["🔍 نتایج جستجو:\n"]

    for user in users:
        user_id = user.user_id

        if user.username:
            user_label = f"@{user.username} | ID: {user_id}"
        else:
            user_label = f"ID: {user_id}"

        lines.append(f"• {user_label}")


    # حالا برای هر کاربر دکمه «اشتراک‌های فعال» اضافه می‌کنیم
    builder = InlineKeyboardBuilder()
    # این‌جا هر ردیف شامل دو دکمه می‌شه: اشتراک‌ها و پیام
    for user in users:
        uname = user.username or f"<کاربر {user.user_id}>"
        builder.row(
            InlineKeyboardButton(
                text=f"📦 اشتراک‌های @{uname}",
                callback_data=f"admin:user_subs:{user.user_id}"
            ),
            InlineKeyboardButton(
                text=f"📩 پیام به @{uname}",
                callback_data=f"admin:message_user:{user.user_id}"
            ),
            InlineKeyboardButton(
                text="🪙 ویرایش اعتبار هدیه",
                callback_data=f"admin:edit_gift:{user.user_id}"
            ),
            width=3
        )
    # یک دکمه بازگشت هم داشته باشیم
    builder.button(text="🔙 بازگشت", callback_data="admin:list_users")
    builder.adjust(1)  # یک ستون

    await msg.answer(
        "\n".join(lines),
        reply_markup=builder.as_markup(),
        parse_mode="HTML"
    )

    await state.clear()



@router.callback_query(F.data.startswith("admin:edit_gift:"))
async def admin_edit_gift_start(cb: CallbackQuery, state: FSMContext):
    _, _, user_id_str = cb.data.split(":")
    user_id = int(user_id_str)

    # واکشی اعتبار هدیه فعلی
    from database.queries import get_user_gift_credit
    current = await get_user_gift_credit(user_id)

    await state.update_data(target_user_id=user_id)
    await cb.answer()

    # دکمه لغو
    cancel_kb = InlineKeyboardMarkup(inline_keyboard=[[
        InlineKeyboardButton(text="❌ لغو", callback_data="admin:cancel_edit_gift")
    ]])

    await cb.message.answer(
        f"🪙 اعتبار هدیه فعلی @{user_id}: `{current:,}` تومان\n"
        "🔢 حالا عدد جدید رو وارد کن:",
        reply_markup=cancel_kb,
        parse_mode="Markdown"
    )
    await state.set_state(EditUserGift.waiting_for_amount)


@router.callback_query(F.data == "admin:cancel_edit_gift")
async def admin_edit_gift_cancel(cb: CallbackQuery, state: FSMContext):
    await state.clear()
    await cb.answer("❌ ویرایش اعتبار هدیه لغو شد.", show_alert=True)
    # می‌تونی کاربر رو برگردونی به منوی قبلی؛ اینجا برمی‌گردیم به منوی جستجو
    await cb.message.edit_text("🔍 جستجو کن یا بازگشت:", reply_markup=InlineKeyboardMarkup(
        inline_keyboard=[[
            InlineKeyboardButton(text="🔙 بازگشت", callback_data="admin:list_users")
        ]]
    ))



@router.message(EditUserGift.waiting_for_amount)
async def admin_edit_gift_receive(msg: types.Message, state: FSMContext):
    data = await state.get_data()
    user_id = data.get("target_user_id")
    text = msg.text.strip()
    if not text.isdigit():
        return await msg.answer("⛔️ فقط عدد وارد کن یا ❌ لغو کن.", reply_markup=msg.reply_markup)

    amount = int(text)
    from database.queries import set_user_gift_credit
    await set_user_gift_credit(user_id, amount)

    await msg.answer(
        f"✅ اعتبار هدیه @{user_id} به `{amount:,}` تومان تغییر کرد!",
        reply_markup=admin_main_menu(),
        parse_mode="Markdown"
    )
    await state.clear()




from aiogram.exceptions import TelegramBadRequest

@router.callback_query(F.data.startswith("admin:user_subs:"))
async def admin_show_user_subs(cb: CallbackQuery):
    await cb.answer()

    # استخراج شناسه کاربر
    _, _, user_id_str = cb.data.split(":")
    user_id = int(user_id_str)

    # واکشی اشتراک‌های فعال
    async with async_session() as session:
        subs = await get_active_orders(user_id, session)

    if not subs:
        return await cb.message.edit_text(f"❌ کاربر 🆔 {user_id} اشتراک فعالی ندارد.")

    # ساخت ردیف‌های کیبورد
    rows: list[list[InlineKeyboardButton]] = []
    for o in subs:
        jalali_date = jdatetime.date.fromgregorian(date=o.expiration_date).strftime("%Y/%m/%d")
        rows.append([
            InlineKeyboardButton(
                text=f"#{o.id} ({o.plan_id}) → {jalali_date}",
                callback_data=f"admin:show_sub:{o.id}"
            ),
            InlineKeyboardButton(
                text="✏️ تغییر تاریخ انقضا",
                callback_data=f"admin:change_exp:{o.id}"
            ),
            InlineKeyboardButton(
                text="🔗 تغییر لینک اختصاصی",
                callback_data=f"admin:assign_link_select:{o.id}"
            )
        ])

    # دکمه بازگشت به همین لیست
    rows.append([
        InlineKeyboardButton(
            text="🔙 بازگشت",
            callback_data="admin:search_user"  # یا "admin:list_users" اگر می‌خواهید به لیست کاربران فعال برگردد
        )
    ])

    kb = InlineKeyboardMarkup(inline_keyboard=rows)
    text = f"📃 اشتراک‌های فعال کاربر 🆔 {user_id}:"

    try:
        await cb.message.edit_text(text, reply_markup=kb)
    except TelegramBadRequest as e:
        error_text = str(e)
        # اگر خطا به دلیل "message is not modified" بود، نادیده بگیر
        if "message is not modified" not in error_text:
            # در غیر این صورت، بگذار خطا بالا بیاید
            raise
        # وگرنه هیچ کاری نکن



from aiogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
import jdatetime
from database.queries import get_order_by_id, get_plan_by_id

@router.callback_query(F.data.startswith("admin:show_sub:"))
async def admin_show_subscription_detail(cb: CallbackQuery):
    await cb.answer()

    # استخراج order_id
    order_id = int(cb.data.split(":")[-1])

    # واکشی سفارش و پلن
    order = await get_order_by_id(order_id)
    plan = await get_plan_by_id(order.plan_id)

    # آماده‌سازی مقادیر
    plan_name = plan.name if plan else "نامشخص"
    # تبدیل تاریخ انقضا به شمسی
    # اگر expiration_date از نوع datetime.datetime است، از date() استفاده کنید:
    greg_date = order.expiration_date
    jalali_date = jdatetime.date.fromgregorian(
        date=greg_date if isinstance(greg_date, jdatetime.datetime) else greg_date
    ).strftime("%Y/%m/%d")
    link = order.assigned_link or "–"

    # متن جزئیات
    text = (
        f"🔍 جزئیات اشتراک #{order.id}:\n"
        f"• پلن: {plan_name}\n"
        f"• تاریخ انقضا (شمسی): {jalali_date}\n"
        f"• لینک اختصاصی: {link}"
    )

    # نمایش متن با دکمه بازگشت به لیست اشتراک‌های کاربر
    await cb.message.edit_text(
        text,
        reply_markup=InlineKeyboardMarkup(inline_keyboard=[[
            InlineKeyboardButton(
                text="🔙 بازگشت",
                callback_data=f"admin:user_subs:{order.user_id}"
            )
        ]])
    )


@router.callback_query(F.data.startswith("admin:change_exp:"))
async def admin_change_exp_cb(cb: CallbackQuery, state: FSMContext):
    await cb.answer()
    order_id = int(cb.data.split(":")[2])
    await state.update_data(order_id=order_id)
    await state.set_state(AdminOrderStates.waiting_for_new_expiry)
    await cb.message.answer(
        f"✏️ لطفاً تاریخ انقضای جدید برای اشتراک #{order_id} را به صورت شمسی `مثلاً 1403/05/25` وارد کنید."
    )


import jdatetime

@router.message(AdminOrderStates.waiting_for_new_expiry)
async def admin_receive_new_expiry(msg: Message, state: FSMContext):
    data = await state.get_data()
    order_id = data["order_id"]

    # تبدیل تاریخ شمسی به میلادی
    try:
        parts = msg.text.strip().split("/")
        if len(parts) != 3:
            raise ValueError("فرمت اشتباه")

        jy, jm, jd = map(int, parts)
        j_date = jdatetime.date(jy, jm, jd)
        new_date = j_date.togregorian()
    except Exception:
        return await msg.reply("❌ لطفاً تاریخ را به صورت 1403/05/25 وارد کنید.")

    # به‌روزرسانی در دیتابیس
    async with async_session() as session:
        await update_order_expiration(order_id, new_date, session)
        await session.commit()

    await msg.reply(f"✅ تاریخ انقضای اشتراک #{order_id} به {msg.text.strip()} (شمسی) تغییر کرد.")
    await state.clear()





@router.callback_query(F.data == "admin:cancel_search")
async def admin_search_user_cancel(cb: CallbackQuery, state: FSMContext):
    await state.clear()
    await cb.message.edit_text(
        "🔙 جستجو کنسل شد.",
        reply_markup=admin_active_users_kb()
    )
    await cb.answer()



from aiogram.fsm.state import State, StatesGroup

# تعریف state جدید
class SendMessageToUser(StatesGroup):
    waiting_for_text = State()

@router.callback_query(F.data.startswith("admin:message_user:"))
async def admin_message_user_start(cb: CallbackQuery, state: FSMContext):
    # استخراج user_id از callback_data
    user_id = int(cb.data.split(":")[-1])
    await state.update_data(target_user_id=user_id)
    await cb.answer()  # حذف loading
    await cb.message.answer(
        f"📝 لطفاً متنی که می‌خوای به کاربر {user_id} ارسال کنی رو اینجا بنویس:"
    )
    await state.set_state(SendMessageToUser.waiting_for_text)



@router.message(SendMessageToUser.waiting_for_text)
async def admin_message_user_send(msg: types.Message, state: FSMContext):
    data = await state.get_data()
    target_user_id = data.get("target_user_id")
    text_to_send = msg.text.strip()

    # فرستادن پیام به کاربر
    try:
        await msg.bot.send_message(
            target_user_id,
            f"📩 پیام از مدیریت:\n\n{text_to_send}"
        )
        await msg.answer("✅ پیام ارسال شد!", reply_markup=admin_main_menu())
    except Exception as e:
        await msg.answer(f"❌ ارسال پیام ناموفق بود:\n{e}", reply_markup=admin_main_menu())

    await state.clear()




@router.callback_query(F.data == "admin:main_menu")
async def admin_return_to_main_menu(cb: CallbackQuery, state: FSMContext):
    await state.clear()
    from bot.keyboards.admin import admin_main_menu
    await cb.message.edit_text(
        "🏠 بازگشت به منوی اصلی ادمین:",
        reply_markup=admin_main_menu()
    )
    await cb.answer()



# bot/handlers/admin.py (پایین سایر router.callback_queryها)

from database.queries import toggle_category_capacity, get_category_by_id
from database.queries import toggle_plan_capacity, get_plan_by_id

@router.callback_query(F.data.startswith("admin:toggle_cat:"))
async def toggle_cat_capacity(cb: CallbackQuery):
    _, _, cat_id, new_state = cb.data.split(":")
    cat_id, new_state = int(cat_id), bool(int(new_state))
    await toggle_category_capacity(cat_id, new_state)
    # آپدیت منو
    cat = await get_category_by_id(cat_id)
    await cb.message.edit_text(
        "چی می‌خوای؟", 
        reply_markup=category_actions_keyboard(cat)
    )
    await cb.answer("وضعیت ظرفیت دسته‌بندی تغییر کرد.", show_alert=True)

@router.callback_query(F.data.startswith("admin:toggle_plan:"))
async def toggle_plan_capacity_cb(cb: CallbackQuery):
    _, _, plan_id, cat_id, new_state = cb.data.split(":")
    plan_id, cat_id, new_state = int(plan_id), int(cat_id), bool(int(new_state))
    await toggle_plan_capacity(plan_id, new_state)
    # آپدیت منو
    pl = await get_plan_by_id(plan_id)
    await cb.message.edit_text(
        "عملیات روی پلن:", 
        reply_markup=plan_actions_keyboard(pl.id, cat_id, is_full=pl.is_full)

    )
    await cb.answer("وضعیت ظرفیت پلن تغییر کرد.", show_alert=True)



def chunked(iterable, size=50):
    chunk = []
    for item in iterable:
        chunk.append(item)
        if len(chunk) == size:
            yield chunk
            chunk = []
    if chunk:
        yield chunk

# ۱) منوی اصلی اطلاع‌رسانی
@router.callback_query(F.data == "admin:notify_menu")
async def notify_menu_cb(cb: CallbackQuery):
    buttons = [
        [InlineKeyboardButton(text="👥 مشتریان فعال", callback_data="admin:notify:active")],
        [InlineKeyboardButton(text="👤 غیر مشتریان",   callback_data="admin:notify:non")],
        [InlineKeyboardButton(text="🌐 همه کاربران", callback_data="admin:notify:all")],
        [InlineKeyboardButton(text="🏠 بازگشت",      callback_data="admin:back")],
    ]
    await cb.message.edit_text("📢 بخش اطلاع‌رسانی: انتخاب کنید به چه کاربرانی پیام داده شود:", 
                               reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
    await cb.answer()

# ۲) انتخاب گروه هدف و ورود به حالت منتظر متن
@router.callback_query(F.data.startswith("admin:notify:"))
async def notify_target_cb(cb: CallbackQuery, state: FSMContext):
    target = cb.data.split(":")[-1]  # "active", "non" یا "all"
    await state.update_data(notify_target=target)
    await cb.message.edit_text("📨 لطفاً متن اطلاع‌رسانی خود را ارسال کنید:")
    await state.set_state(AdminNotificationStates.waiting_for_message)
    await cb.answer()

# ۳) دریافت پیام اطلاع‌رسانی و ارسال به کاربران
from sqlalchemy import select
from aiogram import F
from aiogram.types import Message
from aiogram.fsm.context import FSMContext
from bot.states.states import AdminNotificationStates
from database.db import async_session
# ← تابع درست را ایمپورت می‌کنیم

import asyncio
from database.queries import get_users_by_type

@router.message(
    StateFilter(AdminNotificationStates.waiting_for_message),
    F.from_user.id.in_(ADMINS)
)
async def send_notification_handler(message: Message, state: FSMContext):
    data = await state.get_data()
    target = data.get("notify_target")

    # ⛔ محافظت
    if not message.text:
        await message.reply("⚠️ فقط پیام متنی قابل ارسال است.")
        return

    text = message.text

    async with async_session() as session:
        user_ids = await get_users_by_type(session, target)

    # حذف ادمین‌ها
    user_ids -= set(ADMINS)

    sent = 0
    failed = 0
    blocked = 0

    for batch in chunked(user_ids, 50):
        for uid in batch:
            try:
                await message.bot.send_message(uid, text)
                sent += 1
                await asyncio.sleep(0.1)

            except TelegramForbiddenError:
                blocked += 1
                await mark_user_blocked(uid)

            except TelegramRetryAfter as e:
                await asyncio.sleep(e.retry_after)
                try:
                    await message.bot.send_message(uid, text)
                    sent += 1
                except Exception:
                    failed += 1

            except TelegramBadRequest:
                failed += 1

            except Exception:
                failed += 1

        await asyncio.sleep(1.5)

    await message.reply(
        f"📢 نتیجه اطلاع‌رسانی:\n"
        f"✅ ارسال موفق: {sent}\n"
        f"⛔ بلاک‌شده: {blocked}\n"
        f"⚠️ ناموفق: {failed}"
    )
    await state.clear()






import asyncio
from pathlib import Path
from openpyxl import Workbook
# import pandas as pd
from aiogram import Router, types
from aiogram.filters import Command
from sqlalchemy import text

from database.db import async_session


# فولدر ذخیره گزارش‌ها
REPORT_DIR = Path("data/reports")
REPORT_DIR.mkdir(parents=True, exist_ok=True)
EXCEL_PATH = REPORT_DIR / "all_users_report.xlsx"


# --------------------------
# فرمان get_contact برای یک کاربر
# --------------------------
@router.message(Command("get_contact"))
async def get_contact_handler(msg: types.Message):
    if msg.from_user.id not in ADMINS:
        return await msg.answer("⛔️ شما اجازه‌ی استفاده از این دستور را ندارید.")

    parts = msg.text.strip().split()
    if len(parts) != 2 or not parts[1].isdigit():
        return await msg.answer(
            "ℹ️ دستور صحیح:\n/get_contact <user_id>",
            parse_mode="HTML"
        )

    user_id = int(parts[1])

    # بررسی دیتابیس
    async with async_session() as session:
        user = await session.get(UserPhone, user_id)

    if not user:
        return await msg.answer("❌ کاربر یافت نشد یا شماره ارسال نشده است.")

    text_msg = (
        f"🗒 اطلاعات کاربر <code>{user.user_id}</code>:\n"
        f"📱 <b>شماره تماس:</b> <code>{user.phone_number}</code>\n"
        f"🔗 <b>یوزرنیم:</b> @{user.username or 'ندارد'}\n"
        f"🕓 <b>زمان ثبت:</b> {user.created_at}"
    )
    await msg.answer(text_msg, parse_mode="HTML")


# --------------------------
# تابع کمکی برای ارسال گزارش همه کاربران (از جدول اصلی User)
# --------------------------
async def send_all_contacts_report(user_id: int, bot):
    if user_id not in ADMINS:
        return False, "⛔️ فقط مدیران اجازه استفاده از این دستور را دارند."

    # 🔹 استخراج از جدول اصلی کاربران (User) به جای user_phones
    async with async_session() as session:
        res = await session.execute(select(User))
        users = res.scalars().all()

    if not users:
        return False, "❌ هیچ کاربری یافت نشد."

    try:
        wb = Workbook()
        ws = wb.active
        ws.title = "Users"

        ws.append([
            "user_id",
            "username",
            "gift_credit"
        ])

        for u in users:
            raw_uname = getattr(u, "username", None)
            uname_str = f"@{raw_uname}" if raw_uname else "ندارد"
            
            ws.append([
                str(getattr(u, "user_id", "")),
                uname_str,
                getattr(u, "gift_credit", 0) or 0
            ])

        wb.save(EXCEL_PATH)

    except Exception as e:
        print(f"[ERROR] Excel build error: {e}")
        return False, "❌ خطا در ساخت فایل Excel."

    # ارسال فایل
    try:
        await bot.send_document(
            user_id, 
            types.FSInputFile(str(EXCEL_PATH)),
            caption=f"📊 گزارش کامل تمامی کاربران ربات ({len(users)} کاربر)"
        )
    except Exception as e:
        print(f"[ERROR] Error sending report: {e}")
        return False, "⚠️ خطا در ارسال فایل گزارش."

    return True, "✅ گزارش با موفقیت ارسال شد."


# --------------------------
# فرمان get_contacts برای همه کاربران
# --------------------------
@router.message(Command("get_contacts"))
async def get_contacts_handler(msg: types.Message):
    success, text_msg = await send_all_contacts_report(msg.from_user.id, msg.bot)
    await msg.answer(text_msg)


# --------------------------
# callback دکمه "نمایش لیست کامل کاربرها"
# --------------------------
@router.callback_query(F.data == "admin:get_contacts")
async def admin_get_all_contacts_callback(callback: types.CallbackQuery):
    await callback.answer()
    processing_msg = await callback.message.answer("⏳ در حال پردازش گزارش کاربران...")
    success, text_msg = await send_all_contacts_report(callback.from_user.id, callback.bot)
    try:
        await processing_msg.edit_text(text_msg)
    except Exception:
        pass





from bot.utils.cleanup import cleanup_old_receipts
from bot.keyboards.admin import admin_main_menu

# مرحله اول → سؤال
@router.callback_query(F.data == "admin:cleanup")
async def ask_cleanup_confirm(cb: types.CallbackQuery):
    kb = InlineKeyboardMarkup(inline_keyboard=[
        [
            InlineKeyboardButton(text="✅ بله، پاک کن", callback_data="admin:cleanup_confirm"),
            InlineKeyboardButton(text="❌ خیر، انصراف", callback_data="admin:cleanup_cancel"),
        ]
    ])
    await cb.message.edit_text("❓ آیا مطمئنید می‌خواهید رسیدهای قدیمی حذف شوند؟", reply_markup=kb)
    await cb.answer()


# مرحله دوم → تأیید و پاک‌سازی
@router.callback_query(F.data == "admin:cleanup_confirm")
async def handle_cleanup_confirm(cb: types.CallbackQuery):
    deleted_count = cleanup_old_receipts(folder="receipts", days=60)

    if deleted_count > 0:
        text = f"🧹 پاک‌سازی رسیدها انجام شد.\n📂 {deleted_count} فایل حذف شد ✅"
    else:
        text = "🧹 پاک‌سازی رسیدها انجام شد.\nهیچ فایلی برای حذف پیدا نشد."

    # پیام اصلی دوباره منو بشه
    await cb.message.edit_text("📂 بازگشت به منوی اصلی ادمین:", reply_markup=admin_main_menu())
    # نتیجه به صورت پاپ‌آپ
    await cb.answer(text, show_alert=True)


# مرحله سوم → انصراف
@router.callback_query(F.data == "admin:cleanup_cancel")
async def handle_cleanup_cancel(cb: types.CallbackQuery):
    await cb.message.edit_text("📂 بازگشت به منوی اصلی ادمین:", reply_markup=admin_main_menu())
    await cb.answer("❌ عملیات پاک‌سازی لغو شد.", show_alert=True)



from aiogram import Router, F
from aiogram.types import CallbackQuery
from bot.keyboards.admin import admin_main_menu


# --- بازگشت به منوی اصلی ---
@router.callback_query(F.data == "admin:main_menu")
async def back_to_main_menu(cb: CallbackQuery):
    await cb.message.edit_text(
        "🏠 منوی اصلی مدیریت:",
        reply_markup=admin_main_menu()
    )
    await cb.answer()


# هندلر گزارش خطا وبهوک 
@router.callback_query(F.data == "admin:webhook:errors")
async def show_webhook_errors(callback: CallbackQuery):
    try:
        with open("webhook_errors.log", "r", encoding="utf-8") as f:
            lines = f.readlines()[-100:]  # آخرین 100 خط
        log_text = "".join(lines) or "📭 هیچ خطایی ثبت نشده است."
        if len(log_text) > 4000:
            log_text = log_text[-4000:]
        await callback.message.answer(f"🧾 <b>آخرین گزارش خطاها:</b>\n\n<pre>{log_text}</pre>", parse_mode="HTML")
    except FileNotFoundError:
        await callback.message.answer("📭 هنوز هیچ گزارشی از خطا ثبت نشده است.")

# هندلر باز کردن منوی تنظیمات وبهوک از منوی اصلی ادمین
from aiogram import F
from aiogram.types import CallbackQuery
from bot.handlers.admin_webhook import webhook_menu

@router.callback_query(F.data == "admin:webhook_menu")
async def open_webhook_settings(callback: CallbackQuery):
    """انتقال به منوی تنظیمات وبهوک"""
    await webhook_menu(callback)







from database.queries import set_payment_config
from database.queries import set_payment_config, get_payment_config
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from aiogram import F
from aiogram.types import CallbackQuery, Message
import logging


# ماژولار کردن اطلاعات پرداخت در پنل ادمین
@router.callback_query(F.data == "admin:payment_menu")
async def admin_payment_menu(cb: CallbackQuery):
    logger.info("💳 admin_payment_menu triggered by %s", cb.from_user.id)

    # دریافت اطلاعات فعلی پرداخت
    config = await get_payment_config()
    card_number = config.card_number if config and config.card_number else "ثبت نشده"
    card_holder = config.card_holder if config and config.card_holder else "ثبت نشده"

    text = (
        "💳 مدیریت اطلاعات پرداخت\n\n"
        f"📌 اطلاعات فعلی:\n"
        f"• شماره کارت: {card_number}\n"
        f"• صاحب کارت: {card_holder}\n\n"
        "چه موردی را می‌خواهید ویرایش کنید؟"
    )

    await cb.message.edit_text(
        text,
        reply_markup=InlineKeyboardMarkup(inline_keyboard=[
            [InlineKeyboardButton(
                text="✏️ ویرایش شماره کارت",
                callback_data="admin:payment:edit_card"
            )],
            [InlineKeyboardButton(
                text="✏️ ویرایش نام صاحب کارت",
                callback_data="admin:payment:edit_holder"
            )],
            [InlineKeyboardButton(
                text="🔙 بازگشت",
                callback_data="admin:main_menu"
            )]
        ])
    )
    await cb.answer()

# ویرایش شماره کارت
@router.callback_query(F.data == "admin:payment:edit_card")
async def edit_card_cb(cb: CallbackQuery, state: FSMContext):
    logger.info("✏️ edit_card_cb triggered")

    await state.set_state(PaymentEditStates.waiting_for_card)
    await cb.message.edit_text("✏️ لطفاً شماره کارت جدید را ارسال کنید:")
    await cb.answer()

@router.message(
    PaymentEditStates.waiting_for_card,
    F.from_user.id.in_(ADMINS)
)
async def receive_new_card(msg: Message, state: FSMContext):
    card_number = msg.text.strip()

    # اعتبارسنجی ساده
    if not card_number.isdigit() or len(card_number) != 16:
        await msg.answer("❌ شماره کارت باید ۱۶ رقم عددی باشد.")
        return

    await set_payment_config(card_number=card_number)

    await msg.answer("✅ شماره کارت با موفقیت بروزرسانی شد.")
    await state.clear()

# ویرایش نام حساب
@router.callback_query(F.data == "admin:payment:edit_holder")
async def edit_holder(cb: CallbackQuery, state: FSMContext):
    await cb.message.edit_text("✏️ نام صاحب حساب را ارسال کنید:")
    await state.set_state(PaymentEditStates.waiting_for_holder)
    await cb.answer()

@router.message(
    PaymentEditStates.waiting_for_holder,
    F.from_user.id.in_(ADMINS)
)
async def save_holder(msg: Message, state: FSMContext):
    await set_payment_config(card_holder=msg.text.strip())

    await msg.answer("✅ نام صاحب حساب بروزرسانی شد.")
    await state.clear()


# بکاپ گرفتن از دیتابیس
# ───────────── Advanced Backup System ─────────────
# bot/handlers/admin.py  (یا هر فایلی که router تعریف شده)
# ───────────── Backup Command Handler ─────────────

import asyncio
import logging
from datetime import datetime, timedelta

from aiogram import types
from aiogram.filters import Command

from config import ADMINS
from bot.utils.backup_manager import create_full_backup   # فرض بر این است که تابع در این مسیر است

logger = logging.getLogger(__name__)

# تنظیمات
BACKUP_COOLDOWN_HOURS = 6          # ← می‌توانید این را تغییر دهید یا از config بخوانید
_backup_lock = asyncio.Lock()
_last_backup_time: datetime | None = None


@router.message(Command("backup"))
async def cmd_backup(msg: types.Message):
    if msg.from_user.id not in ADMINS:
        return await msg.answer("⛔️ شما دسترسی ادمین ندارید.")

    if _backup_lock.locked():
        return await msg.answer("⏳ یک عملیات بکاپ دیگر در حال انجام است.\nلطفاً کمی صبر کنید.")

    global _last_backup_time
    now = datetime.utcnow()

    if _last_backup_time:
        time_since_last = now - _last_backup_time
        if time_since_last < timedelta(hours=BACKUP_COOLDOWN_HOURS):
            remaining = timedelta(hours=BACKUP_COOLDOWN_HOURS) - time_since_last
            remaining_str = f"{remaining.seconds // 3600:02d}:{(remaining.seconds % 3600) // 60:02d}"
            return await msg.answer(
                f"⛔️ هنوز نمی‌توانید بکاپ جدید بگیرید.\n"
                f"زمان باقی‌مانده تا بکاپ بعدی: {remaining_str}"
            )

    async with _backup_lock:
        progress_msg = await msg.answer("⏳ آماده‌سازی برای بکاپ کامل...")

        async def update_progress(text: str):
            try:
                await progress_msg.edit_text(f"📦 {text}")
            except Exception:
                pass  # اگر پیام حذف شده بود، بی‌صدا رد می‌شویم

        try:
            # اجرای بکاپ با نمایش پیشرفت
            backup_path = await create_full_backup(progress_callback=update_progress)

            await update_progress("بکاپ ساخته شد – در حال ارسال به ادمین‌ها...")

            sent_success = 0
            sent_failed = 0

            for admin_id in ADMINS:
                try:
                    await msg.bot.send_document(
                        chat_id=admin_id,
                        document=types.FSInputFile(backup_path),
                        caption=(
                            f"🗄 بکاپ کامل پروژه\n"
                            f"نام فایل: {backup_path.name}\n"
                            f"تاریخ: {now.strftime('%Y-%m-%d %H:%M UTC')}\n"
                            f"برای بازیابی از دستور /restore استفاده کنید"
                        )
                    )
                    sent_success += 1
                except Exception as send_err:
                    logger.warning(f"ارسال بکاپ به ادمین {admin_id} ناموفق بود: {send_err}")
                    sent_failed += 1

            status_text = (
                f"✅ بکاپ با موفقیت ساخته شد\n"
                f"فایل: <code>{backup_path.name}</code>\n"
                f"ارسال موفق به {sent_success} نفر"
            )

            if sent_failed > 0:
                status_text += f"\nارسال ناموفق به {sent_failed} نفر"

            await progress_msg.edit_text(status_text, parse_mode="HTML")

            _last_backup_time = now
            logger.info(f"Backup completed: {backup_path.name} – sent to {sent_success} admins")

        except Exception as e:
            logger.exception("خطا در فرآیند بکاپ")
            error_text = f"❌ خطا در ساخت بکاپ:\n{str(e)[:600]}"
            try:
                await progress_msg.edit_text(error_text)
            except:
                await msg.answer(error_text)

        finally:
            if 'backup_path' in locals() and backup_path.exists():
                try:
                    backup_dir = backup_path.parent
                    pattern = "full_backup_*.tar.gz"

                    deleted_count = 0
                    for old_file in backup_dir.glob(pattern):
                        if old_file != backup_path:
                            try:
                                old_file.unlink()
                                deleted_count += 1
                            except Exception as e:
                                logger.warning(f"خطا در حذف {old_file.name}: {e}")

                    if deleted_count > 0:
                        logger.info(f"{deleted_count} بکاپ قدیمی حذف شد. فقط آخرین نسخه نگه داشته شد.")

                except Exception as e:
                    logger.error(f"خطا در پاکسازی بکاپ‌های قدیمی: {e}")



# ایمپورت دیتابیس
# ───────────── Secure Production Restore ─────────────
import asyncio
import shutil
import tarfile
import gzip
import sqlite3
import hashlib
import json
import os
from datetime import datetime
from pathlib import Path

from aiogram.fsm.state import StatesGroup, State
from aiogram.fsm.context import FSMContext
from aiogram.filters import Command
from aiogram.types import Message

from database.db import engine
from config import ADMINS


_restore_lock = asyncio.Lock()


# ───────────── Helpers ─────────────

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


def calculate_sha256(file_path: Path) -> str:
    sha256 = hashlib.sha256()
    with open(file_path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            sha256.update(chunk)
    return sha256.hexdigest()


def get_db_revision(db_path: Path) -> str | None:
    try:
        with sqlite3.connect(db_path) as conn:
            cursor = conn.cursor()
            cursor.execute("SELECT version_num FROM alembic_version LIMIT 1;")
            row = cursor.fetchone()
            return row[0] if row else None
    except Exception:
        return None


# ───────────── FSM ─────────────

class RestoreStates(StatesGroup):
    waiting_for_backup = State()


# ───────────── Command ─────────────

@router.message(Command("restore"))
async def start_restore(msg: Message, state: FSMContext):
    if not is_admin(msg.from_user.id):
        return await msg.answer("⛔️ شما دسترسی ادمین ندارید.")

    if _restore_lock.locked():
        return await msg.answer("⏳ یک عملیات ریستور دیگر در حال انجام است.\nلطفاً صبر کنید.")

    await state.set_state(RestoreStates.waiting_for_backup)
    await msg.answer(
        "📤 لطفاً فایل بکاپ (full_backup_*.tar.gz) را ارسال کنید.\n"
        "⚠️ این عملیات دیتابیس و پوشه‌های مهم را جایگزین می‌کند."
    )


@router.message(RestoreStates.waiting_for_backup)
async def handle_restore(msg: Message, state: FSMContext):
    if not msg.document or not msg.document.file_name.lower().endswith(('.tar.gz', '.tgz')):
        return await msg.answer("❌ لطفاً یک فایل .tar.gz معتبر ارسال کنید.")

    async with _restore_lock:
        PROJECT_DIR = Path(__file__).resolve().parent.parent.parent
        DATA_DIR    = PROJECT_DIR / "data"
        DB_PATH     = DATA_DIR / "database.db"
        TEMP_DIR    = PROJECT_DIR / "restore_temp"

        TEMP_DIR.mkdir(exist_ok=True)
        file_path = TEMP_DIR / msg.document.file_name

        try:
            # ─── مرحله ۱ : دانلود ───
            await msg.answer("📥 در حال دریافت فایل بکاپ...")
            await msg.bot.download(msg.document, destination=file_path)

            # ─── مرحله ۲ : بررسی حداقل نسخه SQLite ───
            sqlite_ver = tuple(map(int, sqlite3.sqlite_version.split(".")))
            if sqlite_ver < (3, 27, 0):
                raise RuntimeError(
                    f"نسخه SQLite شما ({sqlite3.sqlite_version}) قدیمی است.\n"
                    f"حداقل نسخه مورد نیاز: 3.27.0"
                )

            # ─── مرحله ۳ و ۴ : استخراج آرشیو + محاسبه hash محتوای اصلی (بدون metadata) ───
            await msg.answer("📦 استخراج فایل‌ها و بررسی یکپارچگی...")
            temp_content_file = TEMP_DIR / "temp_content_for_hash.tar.gz"

            with tarfile.open(file_path, "r:gz") as src_tar:
                # استخراج همه فایل‌ها برای مراحل بعدی
                src_tar.extractall(
                    path=TEMP_DIR,
                    members=(
                        m for m in src_tar.getmembers()
                        if not (m.name.startswith('/') or '..' in m.name)
                    )
                )

                # ساخت آرشیو موقت فقط از محتوای اصلی (بدون backup_meta.json)
                with tarfile.open(temp_content_file, "w:gz") as dst_tar:
                    for member in src_tar.getmembers():
                        if member.name == "backup_meta.json":
                            continue
                        fileobj = src_tar.extractfile(member)
                        if fileobj is not None:
                            dst_tar.addfile(member, fileobj)

            # محاسبه hash محتوای اصلی
            real_content_hash = calculate_sha256(temp_content_file)

            # پاک کردن فایل موقت
            if temp_content_file.exists():
                temp_content_file.unlink(missing_ok=True)

            # ─── مرحله ۵ : خواندن metadata ───
            meta_file = TEMP_DIR / "backup_meta.json"
            if not meta_file.is_file():
                raise ValueError("فایل metadata (backup_meta.json) در بکاپ یافت نشد.\n"
                                 "این بکاپ با نسخه فعلی سیستم سازگار نیست.")

            try:
                meta = json.loads(meta_file.read_text(encoding="utf-8"))
            except json.JSONDecodeError:
                raise ValueError("فایل metadata خراب یا نامعتبر است.")

            stored_hash = meta.get("sha256")

            # ─── مرحله ۶ : اعتبارسنجی hash (هشدار به جای خطا) ───
            if not stored_hash:
                await msg.answer("⚠️ فایل metadata فاقد فیلد sha256 است.\nادامه می‌دهیم بدون چک hash.")
            else:
                if stored_hash != real_content_hash:
                    await msg.answer(
                        "<b>⚠️ هشدار امنیتی</b>\n\n"
                        "امضای محتوای اصلی بکاپ (دیتابیس + پوشه‌ها بدون metadata) مطابقت ندارد.\n\n"
                        f"ذخیره‌شده: <code>{stored_hash[:16]}...</code>\n"
                        f"محاسبه‌شده: <code>{real_content_hash[:16]}...</code>\n\n"
                        "احتمال بسیار زیاد تغییر فایل توسط تلگرام در زمان ارسال/دریافت است.\n"
                        "→ ریستور ادامه پیدا می‌کند.\n"
                        "→ توصیه می‌شود برای امنیت بیشتر، بکاپ را از طریق لینک مستقیم یا Google Drive منتقل کنید.",
                        parse_mode="HTML"
                    )
                else:
                    await msg.answer("✓ محتوای بکاپ (دیتابیس + پوشه‌ها) بدون تغییر است.")

            # ─── مرحله ۷ : پیدا کردن و استخراج دیتابیس ───
            db_files = list(TEMP_DIR.glob("database_*.db.gz"))
            if not db_files:
                raise FileNotFoundError("فایل دیتابیس فشرده (database_*.db.gz) یافت نشد.")
            if len(db_files) > 1:
                raise ValueError("چند فایل دیتابیس در آرشیو وجود دارد. ساختار بکاپ نامعتبر است.")

            compressed_db = db_files[0]
            extracted_db = TEMP_DIR / compressed_db.stem

            await msg.answer("📂 استخراج دیتابیس...")
            with gzip.open(compressed_db, "rb") as fin, open(extracted_db, "wb") as fout:
                shutil.copyfileobj(fin, fout)

            # ─── مرحله ۸ : مقایسه schema version ───
            current_rev = get_db_revision(DB_PATH) or "نامشخص"
            backup_rev = get_db_revision(extracted_db) or "نامشخص"

            if current_rev != backup_rev:
                raise ValueError(
                    f"نسخه schema مطابقت ندارد\n"
                    f"• دیتابیس فعلی : {current_rev}\n"
                    f"• نسخه داخل بکاپ : {backup_rev}"
                )

            # ─── مرحله ۹ : چک زمان (جلوگیری از downgrade ناخواسته) ───
            if os.path.getmtime(extracted_db) <= os.path.getmtime(DB_PATH):
                raise ValueError("این بکاپ از دیتابیس فعلی قدیمی‌تر است.")

            # ─── مرحله ۱۰ : آماده‌سازی برای جایگزینی ───
            await msg.answer("🔌 قطع اتصالات فعال دیتابیس...")
            await engine.dispose()

            # تعریف پوشه بکاپ‌های احتیاطی
            BACKUP_FOLDER = DATA_DIR / "restore_backups"
            BACKUP_FOLDER.mkdir(exist_ok=True)   # اگر وجود نداشت، ساخته شود

            # بکاپ احتیاطی + نگه داشتن فقط ۱۰ نسخه آخر
            if DB_PATH.exists():
                suffix = datetime.now().strftime("%Y%m%d_%H%M%S")
                backup_before = BACKUP_FOLDER / f"database.before_restore_{suffix}.db"
                shutil.copy(DB_PATH, backup_before)

                # پاک کردن بکاپ‌های قدیمی‌تر از ۱۰ نسخه آخر
                try:
                    MAX_KEEP = 10
                    all_old_backups = sorted(
                        BACKUP_FOLDER.glob("database.before_restore_*.db"),
                        key=lambda p: p.stat().st_mtime,
                        reverse=True  # جدیدترین اول
                    )
                    # نگه داشتن ۱۰ تا اول (جدیدترین‌ها)
                    for old_backup in all_old_backups[MAX_KEEP:]:
                        try:
                            old_backup.unlink()
                        except Exception:
                            pass  # بی‌صدا رد می‌کنیم
                except Exception:
                    pass  # بی‌صدا رد می‌کنیم

            # جایگزینی دیتابیس
            shutil.move(extracted_db, DB_PATH)

            # ─── مرحله ۱۱ : بازیابی پوشه‌ها بر اساس metadata ───
            included_folders = set(meta.get("included_folders", []))

            await msg.answer("📁 بازیابی پوشه‌های اضافی...")

            for folder_rel in ["receipts", "data/reports"]:
                src = TEMP_DIR / folder_rel
                dest = PROJECT_DIR / folder_rel

                if folder_rel in included_folders:
                    if src.exists():
                        if dest.exists():
                            shutil.rmtree(dest, ignore_errors=True)
                        shutil.move(src, dest)
                        await msg.answer(f"✓ پوشه {folder_rel} بازیابی شد")
                    else:
                        await msg.answer(f"⚠️ پوشه {folder_rel} در بکاپ وجود نداشت")
                else:
                    await msg.answer(f"ℹ️ پوشه {folder_rel} در این بکاپ گنجانده نشده بود")

            # ─── پایان موفق ───
            await msg.answer(
                "🎉 ریستور با موفقیت به پایان رسید.\n\n"
                "• دیتابیس و پوشه‌های مهم جایگزین شدند.\n"
                "• پیشنهاد می‌شود ربات را یک‌بار ری‌استارت کنید.\n"
                "• بکاپ احتیاطی از دیتابیس قبلی در کنار database.db ذخیره شده است."
            )

        except Exception as e:
            err_msg = f"❌ خطا در فرآیند ریستور:\n{str(e)}"
            await msg.answer(err_msg, disable_web_page_preview=True)

        finally:
            await state.clear()
            # پاکسازی کامل
            shutil.rmtree(TEMP_DIR, ignore_errors=True)
            if file_path.exists():
                try:
                    file_path.unlink()
                except:
                    pass



# هندلر دکمه های بکاپ در منوی ادمین 
# دکمه ها با تایپ دستور /backup  و  /restore  هم انجام میشوند 
@router.callback_query(F.data == "admin:trigger_backup")
async def trigger_backup_button(callback: CallbackQuery):
    # چک ادمین بودن (اختیاری، اگر می‌خوای فقط ادمین بتونه کلیک کنه)
    if callback.from_user.id not in ADMINS:
        await callback.answer("⛔️ فقط ادمین می‌تواند بکاپ بگیرد", show_alert=True)
        return

    # شبیه‌سازی ارسال دستور /backup توسط کاربر
    fake_msg = callback.message.model_copy(update={
        "text": "/backup",
        "from_user": callback.from_user
    })

    # حالا دقیقاً همان handler دستور /backup رو صدا می‌زنیم
    await cmd_backup(fake_msg)

    # فیدبک به کاربر (اختیاری)
    await callback.answer("دستور ساخت بکاپ ارسال شد ✓")

@router.callback_query(F.data == "admin:trigger_restore")
async def trigger_restore_button(callback: CallbackQuery, state: FSMContext):
    # چک اختیاری ادمین
    if callback.from_user.id not in ADMINS:
        await callback.answer("⛔️ فقط ادمین می‌تواند ریستور کند", show_alert=True)
        return

    # شبیه‌سازی تایپ کردن /restore
    fake_msg = callback.message.model_copy(update={
        "text": "/restore",
        "from_user": callback.from_user
    })

    await start_restore(fake_msg, state)
    await callback.answer("لطفاً فایل بکاپ را ارسال کنید")
    
    
    
    
    
    

# تغییر لینک کاربران
from database.queries import replace_active_links_pattern

# شروع فرآیند تغییر دسته‌جمعی
@router.callback_query(F.data == "admin:link:bulk_replace")
async def bulk_replace_start(cb: CallbackQuery, state: FSMContext):
    if not is_admin(cb.from_user.id):
        return await cb.answer("⛔️ دسترسی نداری.", show_alert=True)

    await cb.message.edit_text(
        "🔄 <b>تغییر دسته‌جمعی آدرس لینک‌ها</b>\n\n"
        "لطفاً <b>عبارت یا دامنه‌ای که می‌خواهید تغییر کند (قدیمی)</b> را ارسال کنید:\n"
        "مثال: <code>old-domain.site</code> یا <code>https://old.com/sub/</code>",
        parse_mode="HTML"
    )
    await state.set_state(BulkLinkReplaceStates.waiting_for_old_text)
    await cb.answer()

# دریافت عبارت قدیمی
@router.message(BulkLinkReplaceStates.waiting_for_old_text)
async def bulk_replace_get_old(msg: Message, state: FSMContext):
    old_str = msg.text.strip()
    await state.update_data(old_str=old_str)

    await msg.answer(
        f"📌 عبارت قدیمی: <code>{html.escape(old_str)}</code>\n\n"
        "حالا <b>عبارت یا دامنه جدید جایگزین</b> را ارسال کنید:",
        parse_mode="HTML"
    )
    await state.set_state(BulkLinkReplaceStates.waiting_for_new_text)

# دریافت عبارت جدید و نمایش پیش‌نمایش جهت تأیید
@router.message(BulkLinkReplaceStates.waiting_for_new_text)
async def bulk_replace_get_new(msg: Message, state: FSMContext):
    new_str = msg.text.strip()
    data = await state.get_data()
    old_str = data["old_str"]
    await state.update_data(new_str=new_str)

    kb = InlineKeyboardMarkup(inline_keyboard=[
        [
            InlineKeyboardButton(text="✅ تأیید و اعمال روی همه", callback_data="admin:link:bulk_confirm"),
            InlineKeyboardButton(text="❌ لغو", callback_data="admin:link:menu")
        ]
    ])

    await msg.answer(
        "⚠️ <b>تأییدیه تغییر دسته‌جمعی لینک‌ها</b>\n\n"
        f"▫️ تبدیل از: <code>{html.escape(old_str)}</code>\n"
        f"▫️ تبدیل به: <code>{html.escape(new_str)}</code>\n\n"
        "با تأیید این عملیات:\n"
        "۱. تمام لینک‌های فعال در دیتابیس ویرایش می‌شوند.\n"
        "۲. به تک‌تک کاربرانی که لینکشان تغییر کرده پیام ارسال خواهد شد.",
        reply_markup=kb,
        parse_mode="HTML"
    )

# اجرای عملیات و اطلاع‌رسانی به کاربران
@router.callback_query(F.data == "admin:link:bulk_confirm")
async def bulk_replace_confirm(cb: CallbackQuery, state: FSMContext):
    data = await state.get_data()
    old_str = data.get("old_str")
    new_str = data.get("new_str")
    await state.clear()

    if not old_str or not new_str:
        await cb.message.edit_text("⚠️ داده‌ها نامعتبر بودند. عملیات لغو شد.", reply_markup=link_pool_menu())
        return

    progress_msg = await cb.message.edit_text("⏳ در حال به‌روزرسانی لینک‌ها در دیتابیس...")

    # ۱. اجرای عملیات در دیتابیس
    updates = await replace_active_links_pattern(old_str, new_str)

    if not updates:
        await progress_msg.edit_text(
            f"ℹ️ هیچ لینک فعالی شامل عبارت <code>{html.escape(old_str)}</code> در دیتابیس یافت نشد و تغییری رخ نداد.",
            reply_markup=link_pool_menu(),
            parse_mode="HTML"
        )
        return

    # ۲. اطلاع‌رسانی به کاربران
    await progress_msg.edit_text(f"✅ {len(updates)} اشتراک با موفقیت تغییر یافت.\n📩 در حال ارسال لینک جدید به کاربران...")

    sent_count = 0
    for item in updates:
        uid = item["user_id"]
        new_link = item["new_link"]
        order_code = item["order_code"]

        msg_text = (
            "🔄 <b>لینک اشتراک شما به‌روزرسانی شد</b>\n\n"
            f"📦 شناسه سفارش: #{order_code}\n"
            "⚠️ به دلیل تغییرات فنی سرور، لطفاً لینک جدید زیر را در نرم‌افزار خود جایگزین کنید:\n\n"
            f"{html.escape(new_link)}"
        )
        try:
            await cb.bot.send_message(uid, msg_text, parse_mode="HTML")
            sent_count += 1
            await asyncio.sleep(0.1)
        except Exception:
            continue

    await progress_msg.edit_text(
        f"🎉 <b>عملیات با موفقیت انجام شد!</b>\n\n"
        f"📊 تعداد لینک‌های ویرایش‌شده در دیتابیس: {len(updates)}\n"
        f"📨 پیام‌های ارسال‌شده به کاربران: {sent_count}",
        reply_markup=link_pool_menu(),
        parse_mode="HTML"
    )