from __future__ import annotations

import asyncio
import uuid
from typing import Any, Awaitable, Callable

from aiogram import BaseMiddleware, F, Router
from aiogram.filters import CommandStart
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message, TelegramObject, Update
from sqlalchemy import func, select

from app import runtime
from app.common import money, parse_signed
from app.config import get_settings
from app.db import get_session
from app.handlers.catalog import _in_stock_ids, _stock_count
from app.keyboards import _chat_url
from app.middlewares import is_subscribed, resolve_join_url
from app.models import (
    Category,
    Item,
    Purchase,
    ResellerBot,
    ResellerCustomer,
    ResellerItemOverride,
    ResellerOrder,
    ResellerPayout,
    ResellerSale,
    Stock,
)
from app.reseller_common import (
    MIN_PAYOUT,
    get_active_reseller,
    record_customer,
    reseller_price,
    reseller_profit,
)
from app.reseller_keyboards import (
    reseller_admin_back_kb,
    reseller_admin_kb,
    reseller_categories_kb,
    reseller_invoice_kb,
    reseller_item_kb,
    reseller_items_kb,
    reseller_main_menu,
    reseller_more_kb,
    reseller_payout_kb,
    reseller_pick_item_kb,
    reseller_sub_gate_kb,
    reseller_twofa_input_kb,
    reseller_twofa_result_kb,
)
from app.services import crypto_pay, totp
from app.states import (
    OtherStates,
    ResellerAdminBroadcast,
    ResellerAdminChannel,
    ResellerAdminImage,
    ResellerAdminMarkup,
)

_SUB_TEXT = (
    "<b>Subscription required</b>\n\n"
    "Join the channel to use this shop, then tap <b>I subscribed</b>."
)


def _is_reseller_admin(reseller: ResellerBot, user_id: int) -> bool:
    return user_id in {reseller.owner_user_id, reseller.admin_user_id}


def _valid_chat_ref(value: str) -> bool:
    value = (value or "").strip()
    if value.startswith("@") and len(value) > 1:
        return True
    if value.startswith(("http://", "https://", "t.me/", "telegram.me/")):
        return True
    return value.lstrip("-").isdigit()


async def _override(
    session, reseller_id: int, item_id: int
) -> ResellerItemOverride | None:
    result = await session.execute(
        select(ResellerItemOverride).where(
            ResellerItemOverride.reseller_id == reseller_id,
            ResellerItemOverride.item_id == item_id,
        )
    )
    return result.scalar_one_or_none()


async def _render_catalog(reseller_id: int):
    async with get_session() as session:
        categories = (
            await session.execute(select(Category).order_by(Category.sort_order, Category.id))
        ).scalars().all()
    if not categories:
        return None
    return "<b>Catalog</b>\n\nChoose a category:", reseller_categories_kb(categories)


async def _render_item_card(reseller_id: int, item_id: int):
    settings = get_settings()
    async with get_session() as session:
        reseller = await get_active_reseller(session, reseller_id)
        item = await session.get(Item, item_id)
        if reseller is None or item is None or not item.is_active:
            return None
        over = await _override(session, reseller_id, item_id)
        if over and over.is_hidden:
            return None
        available = await _stock_count(session, item_id)
        price = reseller_price(item.price, reseller.markup_percent)
        desc = f"\n\n{item.description}" if item.description else ""
        stock_line = (
            f"In stock: <b>{available} pcs.</b>"
            if available
            else "<b>Out of stock</b>"
        )
        text = (
            f"<b>{item.name}</b>{desc}\n\n"
            f"Price: <b>{money(price)} {settings.currency}</b>\n"
            f"{stock_line}"
        )
        kb = reseller_item_kb(item.id, item.category_id, available > 0)
        photo_file_id = over.photo_file_id if over else ""
        return text, kb, photo_file_id


def _format_code(secret: str) -> str:
    code, remaining = totp.generate(secret)
    pretty = f"{code[:3]} {code[3:]}" if len(code) == 6 else code
    return (
        "<b>Google Authenticator</b>\n\n"
        f"Code: <code>{pretty}</code>\n"
        f"Valid for <b>{remaining}</b> sec."
    )


class ResellerSubscriptionMiddleware(BaseMiddleware):
    def __init__(self, reseller_id: int) -> None:
        self.reseller_id = reseller_id

    async def __call__(
        self,
        handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
        event: Update,
        data: dict[str, Any],
    ) -> Any:
        message = event.message
        callback = event.callback_query
        user = message.from_user if message else callback.from_user if callback else None
        if user is None:
            return await handler(event, data)

        async with get_session() as session:
            reseller = await get_active_reseller(session, self.reseller_id)
            if reseller is None:
                return None
            channel = reseller.channel_ref if reseller.channel_required else ""
            is_admin = _is_reseller_admin(reseller, user.id)

        is_recheck = callback is not None and callback.data == "rsubcheck"
        if not channel or is_admin or is_recheck:
            return await handler(event, data)

        bot = data.get("bot") or event.bot
        if await is_subscribed(bot, channel, user.id):
            return await handler(event, data)

        kb = reseller_sub_gate_kb(await resolve_join_url(bot, channel))
        if callback is not None:
            await callback.answer("Subscribe first", show_alert=True)
            try:
                await callback.message.answer(_SUB_TEXT, reply_markup=kb)
            except Exception:
                pass
        elif message is not None:
            await message.answer(_SUB_TEXT, reply_markup=kb)
        return None


def build_reseller_router(reseller_id: int) -> Router:
    router = Router()

    @router.message(CommandStart())
    async def start(message: Message, state: FSMContext) -> None:
        await state.clear()
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
            if reseller is None:
                await message.answer("This shop is unavailable.")
                return
            await record_customer(session, reseller_id, message.from_user)
            is_admin = _is_reseller_admin(reseller, message.from_user.id)
        await message.answer(
            f"Hi, <b>{message.from_user.full_name}</b>!",
            reply_markup=reseller_main_menu(is_admin),
        )

    @router.callback_query(F.data == "rsubcheck")
    async def subcheck(callback: CallbackQuery) -> None:
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
            if reseller is None:
                await callback.answer("Shop unavailable", show_alert=True)
                return
            channel = reseller.channel_ref if reseller.channel_required else ""
            is_admin = _is_reseller_admin(reseller, callback.from_user.id)
        if channel and not is_admin and not await is_subscribed(
            callback.bot, channel, callback.from_user.id
        ):
            await callback.answer("Still not subscribed", show_alert=True)
            return
        try:
            await callback.message.delete()
        except Exception:
            pass
        await callback.message.answer("Thanks, you can use the shop now.")
        await callback.answer()

    @router.message(F.text == "Catalog")
    async def catalog_message(message: Message) -> None:
        rendered = await _render_catalog(reseller_id)
        if rendered is None:
            await message.answer("Catalog is empty.")
            return
        text, kb = rendered
        await message.answer(text, reply_markup=kb)

    @router.callback_query(F.data == "rcatalog")
    async def catalog_callback(callback: CallbackQuery) -> None:
        rendered = await _render_catalog(reseller_id)
        if rendered is None:
            await callback.message.edit_text("Catalog is empty.")
            await callback.answer()
            return
        text, kb = rendered
        await callback.message.edit_text(text, reply_markup=kb)
        await callback.answer()

    @router.callback_query(F.data.startswith("rcat:"))
    async def category(callback: CallbackQuery) -> None:
        settings = get_settings()
        category_id = int(callback.data.split(":")[1])
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
            category = await session.get(Category, category_id)
            if reseller is None or category is None:
                await callback.answer("Category not found", show_alert=True)
                return
            hidden_result = await session.execute(
                select(ResellerItemOverride.item_id).where(
                    ResellerItemOverride.reseller_id == reseller_id,
                    ResellerItemOverride.is_hidden.is_(True),
                )
            )
            hidden_ids = {row[0] for row in hidden_result.all()}
            items = (
                await session.execute(
                    select(Item)
                    .where(Item.category_id == category_id, Item.is_active.is_(True))
                    .order_by(Item.sort_order, Item.id)
                )
            ).scalars().all()
            items = [item for item in items if item.id not in hidden_ids]
            stock_ids = await _in_stock_ids(session, [item.id for item in items])
            prices = {
                item.id: reseller_price(item.price, reseller.markup_percent)
                for item in items
            }
        if not items:
            await callback.message.edit_text(
                f"<b>{category.name}</b>\n\nNo items yet.",
                reply_markup=reseller_items_kb([], settings.currency, {}, set()),
            )
            await callback.answer()
            return
        await callback.message.edit_text(
            f"<b>{category.name}</b>\n\nChoose an item:",
            reply_markup=reseller_items_kb(items, settings.currency, prices, stock_ids),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("ritem:"))
    async def item_card(callback: CallbackQuery) -> None:
        item_id = int(callback.data.split(":")[1])
        rendered = await _render_item_card(reseller_id, item_id)
        if rendered is None:
            await callback.answer("Item unavailable", show_alert=True)
            return
        text, kb, photo_file_id = rendered
        if photo_file_id:
            await callback.message.answer_photo(photo_file_id, caption=text, reply_markup=kb)
        else:
            await callback.message.edit_text(text, reply_markup=kb)
        await callback.answer()

    @router.callback_query(F.data.startswith("rbuy:"))
    async def buy(callback: CallbackQuery) -> None:
        if not runtime.crypto_enabled():
            await callback.answer("Payments are not configured", show_alert=True)
            return
        item_id = int(callback.data.split(":")[1])
        settings = get_settings()
        asset = runtime.crypto_asset()
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
            item = await session.get(Item, item_id)
            if reseller is None or item is None or not item.is_active:
                await callback.answer("Item unavailable", show_alert=True)
                return
            available = await _stock_count(session, item_id)
            if available <= 0:
                await callback.answer("Out of stock", show_alert=True)
                return
            price = reseller_price(item.price, reseller.markup_percent)
            order = ResellerOrder(
                reseller_id=reseller_id,
                buyer_user_id=callback.from_user.id,
                item_id=item_id,
                price=price,
                status="pending",
            )
            session.add(order)
            await session.commit()
            order_id = order.id
            item_name = item.name
        try:
            invoice = await crypto_pay.create_invoice(
                amount=price,
                asset=asset,
                description=f"{item_name}: {money(price)} {settings.currency}",
                payload=f"reseller:{order_id}",
            )
        except Exception:
            await callback.answer("Invoice error", show_alert=True)
            return
        pay_url = (
            invoice.get("bot_invoice_url")
            or invoice.get("pay_url")
            or invoice.get("mini_app_invoice_url", "")
        )
        if not pay_url:
            await callback.answer("Payment link unavailable", show_alert=True)
            return
        async with get_session() as session:
            row = await session.get(ResellerOrder, order_id)
            if row:
                row.invoice_id = str(invoice.get("invoice_id", ""))
                await session.commit()
        await callback.message.answer(
            f"<b>Payment</b>\n\n"
            f"Item: <b>{item_name}</b>\n"
            f"Amount: <b>{money(price)} {asset}</b>\n\n"
            "Pay the invoice, then tap the check button.",
            reply_markup=reseller_invoice_kb(pay_url, order_id),
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("rcheck:"))
    async def check_payment(callback: CallbackQuery) -> None:
        order_id = int(callback.data.split(":")[1])
        settings = get_settings()
        async with get_session() as session:
            order = await session.get(ResellerOrder, order_id)
            if order is None or order.buyer_user_id != callback.from_user.id:
                await callback.answer("Order not found", show_alert=True)
                return
            if order.status == "approved":
                await callback.answer("Already delivered", show_alert=True)
                return
            if not order.invoice_id:
                await callback.answer("Invoice missing", show_alert=True)
                return
            invoice_id = order.invoice_id
        try:
            invoice = await crypto_pay.get_invoice(invoice_id)
        except Exception:
            await callback.answer("Check failed, try again", show_alert=True)
            return
        if not invoice or invoice.get("status") != "paid":
            await callback.answer("Payment not received yet", show_alert=True)
            return

        async with get_session() as session:
            order = await session.get(ResellerOrder, order_id)
            if order is None or order.status == "approved":
                await callback.answer("Already processed", show_alert=True)
                return
            reseller = await session.get(ResellerBot, order.reseller_id)
            item = await session.get(Item, order.item_id)
            if reseller is None or item is None:
                await callback.answer("Order error", show_alert=True)
                return
            stock_result = await session.execute(
                select(Stock)
                .where(Stock.item_id == item.id, Stock.is_sold.is_(False))
                .order_by(Stock.id)
                .limit(1)
                .with_for_update()
            )
            stock = stock_result.scalar_one_or_none()
            if stock is None:
                order.status = "no_stock"
                await session.commit()
                await callback.message.edit_text(
                    "Payment received, but the item is out of stock now. "
                    "Please contact support."
                )
                await callback.answer("Out of stock", show_alert=True)
                return

            stock.is_sold = True
            final_price = order.price
            reseller_cut, platform_cut = reseller_profit(item.price, final_price)
            reseller.earnings_balance += reseller_cut
            purchase = Purchase(
                user_id=callback.from_user.id,
                item_id=item.id,
                reseller_id=reseller.id,
                item_name=item.name,
                price=final_price,
                content=stock.content,
                totp_secret=stock.totp_secret,
            )
            session.add(purchase)
            await session.flush()
            sale = ResellerSale(
                reseller_id=reseller.id,
                buyer_user_id=callback.from_user.id,
                item_id=item.id,
                item_name=item.name,
                base_price=item.price,
                reseller_price=final_price,
                reseller_profit=reseller_cut,
                platform_profit=platform_cut,
                purchase_id=purchase.id,
            )
            session.add(sale)
            order.status = "approved"
            await session.commit()
            content = stock.content
            item_name = item.name
            new_profit_balance = reseller.earnings_balance
            has_2fa = bool(stock.totp_secret)
            purchase_id = purchase.id

        note = "\n\nThis item has 2FA. Use the button in purchases to get a code." if has_2fa else ""
        await callback.message.edit_text(
            f"<b>Purchase successful!</b>\n\n"
            f"{item_name}\n"
            f"Charged: {money(order.price)} {settings.currency}\n\n"
            f"Your item:\n<code>{content}</code>{note}"
        )
        try:
            await callback.bot.send_message(
                reseller.admin_user_id,
                f"New sale: <b>{item_name}</b>\n"
                f"Your profit: <b>{money(reseller_cut)} {settings.currency}</b>\n"
                f"Balance: <b>{money(new_profit_balance)} {settings.currency}</b>",
            )
        except Exception:
            pass
        await callback.answer("Delivered")

    @router.message(F.text == "My purchases")
    async def purchases(message: Message) -> None:
        settings = get_settings()
        async with get_session() as session:
            rows = (
                await session.execute(
                    select(Purchase)
                    .where(
                        Purchase.user_id == message.from_user.id,
                        Purchase.reseller_id == reseller_id,
                    )
                    .order_by(Purchase.created_at.desc())
                    .limit(15)
                )
            ).scalars().all()
        if not rows:
            await message.answer("You have no purchases yet.")
            return
        lines = ["<b>Recent purchases</b>\n"]
        for p in rows:
            date = p.created_at.strftime("%d.%m.%Y %H:%M")
            lock = " 2FA" if p.totp_secret else ""
            lines.append(
                f"<b>{p.item_name}</b>{lock} - {money(p.price)} {settings.currency} ({date})\n"
                f"<code>{p.content}</code>"
            )
        await message.answer("\n\n".join(lines))

    async def _more_markup(bot):
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
            channel = reseller.channel_ref if reseller else ""
        support = _chat_url(runtime.support_url())
        channel_url = await resolve_join_url(bot, channel) if channel else ""
        return reseller_more_kb(support, channel_url)

    @router.message(F.text == "More")
    async def more(message: Message, state: FSMContext) -> None:
        await state.clear()
        await message.answer("<b>More</b>", reply_markup=await _more_markup(message.bot))

    @router.callback_query(F.data == "rother:menu")
    async def more_cb(callback: CallbackQuery, state: FSMContext) -> None:
        await state.clear()
        await callback.message.edit_text(
            "<b>More</b>", reply_markup=await _more_markup(callback.bot)
        )
        await callback.answer()

    @router.callback_query(F.data == "rother:2fa")
    async def twofa_start(callback: CallbackQuery, state: FSMContext) -> None:
        await state.set_state(OtherStates.twofa)
        await callback.message.edit_text(
            "<b>Google Authenticator</b>\n\nSend your 2FA secret.",
            reply_markup=reseller_twofa_input_kb(),
        )
        await callback.answer()

    @router.message(OtherStates.twofa)
    async def twofa_input(message: Message, state: FSMContext) -> None:
        secret = totp.normalize_secret(message.text or "")
        if not totp.is_valid_secret(secret):
            await message.answer("Invalid 2FA secret.", reply_markup=reseller_twofa_input_kb())
            return
        await state.update_data(twofa_secret=secret)
        await message.answer(_format_code(secret), reply_markup=reseller_twofa_result_kb())

    @router.callback_query(OtherStates.twofa, F.data == "rotpgen:ref")
    async def twofa_refresh(callback: CallbackQuery, state: FSMContext) -> None:
        data = await state.get_data()
        secret = data.get("twofa_secret", "")
        if not secret:
            await callback.answer("Enter the secret again", show_alert=True)
            return
        try:
            await callback.message.edit_text(
                _format_code(secret), reply_markup=reseller_twofa_result_kb()
            )
        except Exception:
            pass
        await callback.answer("Refreshed")

    @router.message(F.text == "Admin panel")
    async def admin_panel(message: Message, state: FSMContext) -> None:
        await state.clear()
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
            if reseller is None or not _is_reseller_admin(reseller, message.from_user.id):
                return
        await message.answer("<b>Admin panel</b>", reply_markup=reseller_admin_kb())

    @router.callback_query(F.data == "ra:menu")
    async def admin_menu(callback: CallbackQuery, state: FSMContext) -> None:
        await state.clear()
        await callback.message.edit_text("<b>Admin panel</b>", reply_markup=reseller_admin_kb())
        await callback.answer()

    async def _require_admin(callback: CallbackQuery) -> ResellerBot | None:
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
        if reseller is None or not _is_reseller_admin(reseller, callback.from_user.id):
            await callback.answer()
            return None
        return reseller

    @router.callback_query(F.data == "ra:markup")
    async def markup_start(callback: CallbackQuery, state: FSMContext) -> None:
        reseller = await _require_admin(callback)
        if reseller is None:
            return
        await state.set_state(ResellerAdminMarkup.value)
        await callback.message.edit_text(
            f"Current markup: <b>{money(reseller.markup_percent)}%</b>\n\n"
            "Send a new percent, e.g. <code>20</code>.",
            reply_markup=reseller_admin_back_kb(),
        )
        await callback.answer()

    @router.message(ResellerAdminMarkup.value)
    async def markup_save(message: Message, state: FSMContext) -> None:
        value = parse_signed(message.text or "")
        if value is None or value < 0:
            await message.answer("Send a number from 0 and higher.")
            return
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
            if reseller is None or not _is_reseller_admin(reseller, message.from_user.id):
                return
            reseller.markup_percent = round(value, 2)
            await session.commit()
        await state.clear()
        await message.answer("Markup saved.", reply_markup=reseller_admin_kb())

    @router.callback_query(F.data == "ra:channel")
    async def channel_start(callback: CallbackQuery, state: FSMContext) -> None:
        reseller = await _require_admin(callback)
        if reseller is None:
            return
        shown = reseller.channel_ref or "not set"
        await state.set_state(ResellerAdminChannel.value)
        await callback.message.edit_text(
            f"Current channel: <code>{shown}</code>\n\n"
            "Send @channel, link, numeric id, or <code>-</code> to disable. "
            "If set, buyers must subscribe and the channel appears in More.",
            reply_markup=reseller_admin_back_kb(),
        )
        await callback.answer()

    @router.message(ResellerAdminChannel.value)
    async def channel_save(message: Message, state: FSMContext) -> None:
        value = (message.text or "").strip()
        if value == "-":
            value = ""
        elif not _valid_chat_ref(value):
            await message.answer("Send @channel, link, numeric id, or -.")
            return
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
            if reseller is None or not _is_reseller_admin(reseller, message.from_user.id):
                return
            reseller.channel_ref = value
            reseller.channel_required = bool(value)
            await session.commit()
        await state.clear()
        await message.answer("Channel settings saved.", reply_markup=reseller_admin_kb())

    @router.callback_query(F.data == "ra:images")
    async def images_start(callback: CallbackQuery) -> None:
        reseller = await _require_admin(callback)
        if reseller is None:
            return
        async with get_session() as session:
            items = (
                await session.execute(select(Item).order_by(Item.id))
            ).scalars().all()
        await callback.message.edit_text(
            "Choose item:", reply_markup=reseller_pick_item_kb(items)
        )
        await callback.answer()

    @router.callback_query(F.data.startswith("ra:img:"))
    async def image_pick(callback: CallbackQuery, state: FSMContext) -> None:
        reseller = await _require_admin(callback)
        if reseller is None:
            return
        item_id = int(callback.data.split(":")[2])
        await state.set_state(ResellerAdminImage.photo)
        await state.update_data(item_id=item_id)
        await callback.message.edit_text(
            "Send a product photo, or <code>-</code> to remove it.",
            reply_markup=reseller_admin_back_kb(),
        )
        await callback.answer()

    @router.message(ResellerAdminImage.photo)
    async def image_save(message: Message, state: FSMContext) -> None:
        data = await state.get_data()
        item_id = int(data.get("item_id", 0))
        remove = (message.text or "").strip() == "-"
        if not remove and not message.photo:
            await message.answer("Send a photo or -.")
            return
        photo_file_id = "" if remove else message.photo[-1].file_id
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
            if reseller is None or not _is_reseller_admin(reseller, message.from_user.id):
                return
            over = await _override(session, reseller_id, item_id)
            if over is None:
                over = ResellerItemOverride(reseller_id=reseller_id, item_id=item_id)
                session.add(over)
            over.photo_file_id = photo_file_id
            await session.commit()
        await state.clear()
        await message.answer("Image saved.", reply_markup=reseller_admin_kb())

    @router.callback_query(F.data == "ra:broadcast")
    async def broadcast_start(callback: CallbackQuery, state: FSMContext) -> None:
        reseller = await _require_admin(callback)
        if reseller is None:
            return
        await state.set_state(ResellerAdminBroadcast.message)
        await callback.message.edit_text(
            "Send broadcast text:", reply_markup=reseller_admin_back_kb()
        )
        await callback.answer()

    @router.message(ResellerAdminBroadcast.message)
    async def broadcast_send(message: Message, state: FSMContext) -> None:
        await state.clear()
        text = message.text or ""
        async with get_session() as session:
            reseller = await get_active_reseller(session, reseller_id)
            if reseller is None or not _is_reseller_admin(reseller, message.from_user.id):
                return
            user_ids = [
                row[0]
                for row in (
                    await session.execute(
                        select(ResellerCustomer.user_id).where(
                            ResellerCustomer.reseller_id == reseller_id
                        )
                    )
                ).all()
            ]
        await message.answer(f"Broadcast started to {len(user_ids)} users.")
        sent = 0
        failed = 0
        for uid in user_ids:
            try:
                await message.bot.send_message(uid, text)
                sent += 1
            except Exception:
                failed += 1
            await asyncio.sleep(0.05)
        await message.answer(
            f"Done. Delivered: {sent}, failed: {failed}.",
            reply_markup=reseller_admin_kb(),
        )

    @router.callback_query(F.data == "ra:balance")
    async def balance(callback: CallbackQuery) -> None:
        reseller = await _require_admin(callback)
        if reseller is None:
            return
        settings = get_settings()
        async with get_session() as session:
            sales = await session.scalar(
                select(func.count()).select_from(ResellerSale).where(
                    ResellerSale.reseller_id == reseller_id
                )
            )
        can = reseller.earnings_balance >= MIN_PAYOUT
        await callback.message.edit_text(
            "<b>Balance</b>\n\n"
            f"Earned balance: <b>{money(reseller.earnings_balance)} {settings.currency}</b>\n"
            f"Sales: <b>{sales or 0}</b>\n"
            f"Minimum payout: <b>{money(MIN_PAYOUT)} {settings.currency}</b>",
            reply_markup=reseller_payout_kb(can),
        )
        await callback.answer()

    @router.callback_query(F.data == "ra:payout")
    async def payout(callback: CallbackQuery) -> None:
        reseller = await _require_admin(callback)
        if reseller is None:
            return
        if not runtime.crypto_enabled():
            await callback.answer("CryptoBot is not configured", show_alert=True)
            return
        if reseller.earnings_balance < MIN_PAYOUT:
            await callback.answer("Minimum payout is $3", show_alert=True)
            return
        asset = runtime.crypto_asset()
        amount = round(reseller.earnings_balance, 2)
        spend_id = f"reseller-{reseller.id}-{uuid.uuid4().hex[:24]}"
        async with get_session() as session:
            fresh = await session.get(ResellerBot, reseller.id)
            if fresh is None or fresh.earnings_balance < MIN_PAYOUT:
                await callback.answer("Balance changed, try again", show_alert=True)
                return
            payout_row = ResellerPayout(
                reseller_id=reseller.id,
                amount=amount,
                asset=asset,
                status="pending",
                spend_id=spend_id,
            )
            session.add(payout_row)
            fresh.earnings_balance = 0.0
            await session.commit()
            payout_id = payout_row.id
        try:
            await crypto_pay.transfer(
                user_id=reseller.admin_user_id,
                amount=amount,
                asset=asset,
                spend_id=spend_id,
                comment="Reseller payout",
            )
        except Exception:
            async with get_session() as session:
                fresh = await session.get(ResellerBot, reseller.id)
                payout_row = await session.get(ResellerPayout, payout_id)
                if fresh:
                    fresh.earnings_balance += amount
                if payout_row:
                    payout_row.status = "failed"
                await session.commit()
            await callback.answer("Payout failed", show_alert=True)
            return
        async with get_session() as session:
            payout_row = await session.get(ResellerPayout, payout_id)
            if payout_row:
                payout_row.status = "paid"
                await session.commit()
        await callback.message.edit_text(
            f"Payout sent: <b>{money(amount)} {asset}</b>.",
            reply_markup=reseller_admin_kb(),
        )
        await callback.answer("Paid")

    return router
