from __future__ import annotations

import asyncio

from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app import leaderboard as leaderboard_svc, runtime
from app.common import is_admin, money, parse_amount, parse_signed
from app.config import get_settings
from app.db import get_session
from app.handlers.catalog import _stock_count
from app.keyboards import (
    admin_menu,
    back_admin_kb,
    buy_now_kb,
    cancel_kb,
    item_edit_menu_kb,
    pick_list_kb,
    promo_confirm_kb,
    settings_menu_kb,
    topup_review_kb,
)
from app.models import Category, Item, Purchase, Stock, TopUp, User
from app.services import totp
from app.states import (
    AdminBalance,
    AdminBroadcast,
    AdminCategory,
    AdminItem,
    AdminItemEdit,
    AdminSettings,
    AdminStock,
)

router = Router()


def _admin_only(user_id: int) -> bool:
    return is_admin(user_id)


async def _render_cat_list(callback: CallbackQuery) -> None:
    async with get_session() as session:
        result = await session.execute(select(Category).order_by(Category.id))
        cats = result.scalars().all()
    if not cats:
        await callback.message.edit_text("No categories.", reply_markup=back_admin_kb())
        return
    rows = [(f"× {c.name}", f"delcat:{c.id}") for c in cats]
    await callback.message.edit_text(
        "Choose a category to delete (its items will be deleted too):",
        reply_markup=pick_list_kb(rows),
    )


async def _render_item_list(callback: CallbackQuery) -> None:
    async with get_session() as session:
        result = await session.execute(select(Item).order_by(Item.id))
        items = result.scalars().all()
    if not items:
        await callback.message.edit_text("No items.", reply_markup=back_admin_kb())
        return
    rows = [(f"× {i.name}", f"delitem:{i.id}") for i in items]
    await callback.message.edit_text(
        "Choose an item to delete:", reply_markup=pick_list_kb(rows)
    )


# ---------- Admin entry ----------


@router.message(F.text == "◆ Admin panel")
@router.message(Command("admin"))
async def admin_panel(message: Message, state: FSMContext) -> None:
    if not _admin_only(message.from_user.id):
        return
    await state.clear()
    await message.answer("🛠 <b>Admin panel</b>", reply_markup=admin_menu())


@router.message(Command("payout"))
async def force_payout(message: Message) -> None:
    """Manually run the weekly leaderboard payout + reset (for testing)."""
    if not _admin_only(message.from_user.id):
        return
    winners = await leaderboard_svc.run_payout(message.bot)
    settings = get_settings()
    if not winners:
        await message.answer("🏆 Payout run: no participants this week. Leaderboard reset.")
        return
    lines = ["🏆 <b>Payout done — leaderboard reset</b>\n"]
    for w in winners:
        lines.append(
            f"#{w['place']} — <code>{w['user_id']}</code> "
            f"(+{money(w['reward'])} {settings.currency}, spent {w['spent']})"
        )
    await message.answer("\n".join(lines))


@router.callback_query(F.data == "a:menu")
async def admin_menu_cb(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    await state.clear()
    await callback.message.edit_text("🛠 <b>Admin panel</b>", reply_markup=admin_menu())
    await callback.answer()


# ---------- Statistics ----------


@router.callback_query(F.data == "a:stats")
async def admin_stats(callback: CallbackQuery) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    settings = get_settings()
    async with get_session() as session:
        users = await session.scalar(select(func.count()).select_from(User))
        items = await session.scalar(select(func.count()).select_from(Item))
        sales = await session.scalar(select(func.count()).select_from(Purchase))
        revenue = await session.scalar(select(func.coalesce(func.sum(Purchase.price), 0)))
        stock_left = await session.scalar(
            select(func.count()).select_from(Stock).where(Stock.is_sold.is_(False))
        )
    await callback.message.edit_text(
        "📊 <b>Statistics</b>\n\n"
        f"👥 Users: <b>{users}</b>\n"
        f"📦 Items: <b>{items}</b>\n"
        f"📥 Stock positions: <b>{stock_left}</b>\n"
        f"🧾 Sales: <b>{sales}</b>\n"
        f"💰 Revenue: <b>{money(revenue)} {settings.currency}</b>",
        reply_markup=back_admin_kb(),
    )
    await callback.answer()


# ---------- Categories ----------


@router.callback_query(F.data == "a:addcat")
async def add_cat_start(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    await state.set_state(AdminCategory.name)
    await callback.message.edit_text(
        "➕ Enter the category name:", reply_markup=cancel_kb()
    )
    await callback.answer()


@router.message(AdminCategory.name)
async def add_cat_save(message: Message, state: FSMContext) -> None:
    name = (message.text or "").strip()
    if not name:
        await message.answer("❌ The name cannot be empty.")
        return
    await state.clear()
    async with get_session() as session:
        session.add(Category(name=name))
        await session.commit()
    await message.answer(f"✅ Category “{name}” added.", reply_markup=back_admin_kb())


@router.callback_query(F.data == "a:delcat")
async def del_cat_list(callback: CallbackQuery) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    await _render_cat_list(callback)
    await callback.answer()


@router.callback_query(F.data.startswith("delcat:"))
async def del_cat_do(callback: CallbackQuery) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    cat_id = int(callback.data.split(":")[1])
    async with get_session() as session:
        cat = await session.get(Category, cat_id)
        if cat:
            await session.delete(cat)
            await session.commit()
    await callback.answer("Deleted")
    await _render_cat_list(callback)


# ---------- Items ----------


@router.callback_query(F.data == "a:additem")
async def add_item_start(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    async with get_session() as session:
        result = await session.execute(select(Category).order_by(Category.id))
        cats = result.scalars().all()
    if not cats:
        await callback.message.edit_text(
            "Create a category first.", reply_markup=back_admin_kb()
        )
        await callback.answer()
        return
    await state.set_state(AdminItem.category)
    rows = [(c.name, f"pickcat:{c.id}") for c in cats]
    await callback.message.edit_text(
        "➕ Which category to add the item to?", reply_markup=pick_list_kb(rows)
    )
    await callback.answer()


@router.callback_query(AdminItem.category, F.data.startswith("pickcat:"))
async def add_item_cat(callback: CallbackQuery, state: FSMContext) -> None:
    cat_id = int(callback.data.split(":")[1])
    await state.update_data(category_id=cat_id)
    await state.set_state(AdminItem.name)
    await callback.message.edit_text("Enter the item name:", reply_markup=cancel_kb())
    await callback.answer()


@router.message(AdminItem.name)
async def add_item_name(message: Message, state: FSMContext) -> None:
    name = (message.text or "").strip()
    if not name:
        await message.answer("❌ The name cannot be empty.")
        return
    await state.update_data(name=name)
    await state.set_state(AdminItem.description)
    await message.answer("Enter a description (or “-” to skip):")


@router.message(AdminItem.description)
async def add_item_desc(message: Message, state: FSMContext) -> None:
    desc = (message.text or "").strip()
    if desc == "-":
        desc = ""
    await state.update_data(description=desc)
    await state.set_state(AdminItem.price)
    await message.answer("Enter the price (whole number):")


@router.message(AdminItem.price)
async def add_item_price(message: Message, state: FSMContext) -> None:
    price = parse_amount(message.text or "")
    if price is None:
        await message.answer("❌ Enter a positive number (e.g. 10 or 0.5).")
        return
    data = await state.get_data()
    await state.clear()
    async with get_session() as session:
        item = Item(
            category_id=data["category_id"],
            name=data["name"],
            description=data.get("description", ""),
            price=price,
        )
        session.add(item)
        await session.commit()
        item_id = item.id
    await message.answer(
        f"✅ Item “{data['name']}” added (id {item_id}).\n"
        "Don't forget to upload stock via “📦 Add stock”.",
        reply_markup=back_admin_kb(),
    )


# ---------- Edit item ----------

_ITEM_FIELDS = {
    "name": ("name", "✎ Send the new <b>name</b>:"),
    "desc": ("description", "✎ Send the new <b>description</b> (or “-” to clear):"),
    "price": ("price", "✎ Send the new <b>price</b> (e.g. 10 or 0.5):"),
}


async def _render_item_edit(target, item_id: int, *, edit: bool) -> None:
    """Show the edit card for one item. `target` is a Message-like with edit/answer."""
    settings = get_settings()
    async with get_session() as session:
        item = await session.get(Item, item_id)
    if item is None:
        await target.edit_text("Item not found.", reply_markup=back_admin_kb())
        return
    status = "✅ active" if item.is_active else "🚫 hidden"
    desc = item.description or "—"
    text = (
        f"✎ <b>Edit item</b>\n\n"
        f"🛒 <b>{item.name}</b>\n"
        f"💵 Price: <b>{money(item.price)} {settings.currency}</b>\n"
        f"📝 Description: {desc}\n"
        f"👁 Status: {status}\n\n"
        "Pick a field to change:"
    )
    kb = item_edit_menu_kb(item_id, item.is_active)
    if edit:
        await target.edit_text(text, reply_markup=kb)
    else:
        await target.answer(text, reply_markup=kb)


@router.callback_query(F.data == "a:edititem")
async def edit_item_list(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    await state.clear()
    async with get_session() as session:
        result = await session.execute(select(Item).order_by(Item.id))
        items = result.scalars().all()
    if not items:
        await callback.message.edit_text("No items yet.", reply_markup=back_admin_kb())
        await callback.answer()
        return
    rows = [(f"{'✅' if i.is_active else '🚫'} {i.name}", f"editi:{i.id}") for i in items]
    await callback.message.edit_text(
        "✎ Choose an item to edit:", reply_markup=pick_list_kb(rows)
    )
    await callback.answer()


@router.callback_query(F.data.startswith("editi:"))
async def edit_item_open(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    await state.clear()
    item_id = int(callback.data.split(":")[1])
    await _render_item_edit(callback.message, item_id, edit=True)
    await callback.answer()


@router.callback_query(F.data.startswith("editf:toggle:"))
async def edit_item_toggle(callback: CallbackQuery) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    item_id = int(callback.data.split(":")[2])
    async with get_session() as session:
        item = await session.get(Item, item_id)
        if item is None:
            await callback.answer("Item not found", show_alert=True)
            return
        item.is_active = not item.is_active
        await session.commit()
        state_txt = "shown" if item.is_active else "hidden"
    await _render_item_edit(callback.message, item_id, edit=True)
    await callback.answer(f"Item {state_txt}")


@router.callback_query(F.data.startswith("editf:"))
async def edit_item_field_start(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    _, field, item_id_raw = callback.data.split(":")
    if field not in _ITEM_FIELDS:
        await callback.answer()
        return
    await state.set_state(AdminItemEdit.value)
    await state.update_data(edit_field=field, item_id=int(item_id_raw))
    _, prompt = _ITEM_FIELDS[field]
    await callback.message.edit_text(prompt, reply_markup=cancel_kb())
    await callback.answer()


@router.message(AdminItemEdit.value)
async def edit_item_field_save(message: Message, state: FSMContext) -> None:
    if not _admin_only(message.from_user.id):
        return
    data = await state.get_data()
    field = data.get("edit_field", "")
    item_id = int(data.get("item_id", 0))
    if field not in _ITEM_FIELDS or item_id <= 0:
        await state.clear()
        await message.answer("Session expired.", reply_markup=back_admin_kb())
        return
    attr, _ = _ITEM_FIELDS[field]
    raw = (message.text or "").strip()
    if field == "name":
        if not raw:
            await message.answer("❌ The name cannot be empty.")
            return
        new_value = raw
    elif field == "desc":
        new_value = "" if raw == "-" else raw
    else:  # price
        price = parse_amount(raw)
        if price is None:
            await message.answer("❌ Enter a positive number (e.g. 10 or 0.5).")
            return
        new_value = price
    await state.clear()
    async with get_session() as session:
        item = await session.get(Item, item_id)
        if item is None:
            await message.answer("Item not found.", reply_markup=back_admin_kb())
            return
        setattr(item, attr, new_value)
        await session.commit()
    await message.answer("✅ Updated.")
    await _render_item_edit(message, item_id, edit=False)


@router.callback_query(F.data == "a:delitem")
async def del_item_list(callback: CallbackQuery) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    await _render_item_list(callback)
    await callback.answer()


@router.callback_query(F.data.startswith("delitem:"))
async def del_item_do(callback: CallbackQuery) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    item_id = int(callback.data.split(":")[1])
    async with get_session() as session:
        item = await session.get(Item, item_id)
        if item:
            await session.delete(item)
            await session.commit()
    await callback.answer("Deleted")
    await _render_item_list(callback)


# ---------- Stock ----------


@router.callback_query(F.data == "a:addstock")
async def add_stock_start(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    async with get_session() as session:
        result = await session.execute(select(Item).order_by(Item.id))
        items = result.scalars().all()
    if not items:
        await callback.message.edit_text(
            "Create an item first.", reply_markup=back_admin_kb()
        )
        await callback.answer()
        return
    await state.set_state(AdminStock.item)
    rows = [(i.name, f"pickitem:{i.id}") for i in items]
    await callback.message.edit_text(
        "📦 Which item to add stock for?", reply_markup=pick_list_kb(rows)
    )
    await callback.answer()


@router.callback_query(AdminStock.item, F.data.startswith("pickitem:"))
async def add_stock_item(callback: CallbackQuery, state: FSMContext) -> None:
    item_id = int(callback.data.split(":")[1])
    await state.update_data(item_id=item_id)
    await state.set_state(AdminStock.content)
    await callback.message.edit_text(
        "📦 Send the positions — as <b>text</b> or a <b>.txt file</b>.\n\n"
        "Positions are separated by a line with the <code>&</code> symbol.\n"
        "One block = one unit of the item (can be multi-line).\n\n"
        "🔐 To attach 2FA, add a line to the block:\n"
        "<code>2fa: YOUR_SECRET</code>\n\n"
        "Example:\n"
        "<code>login1:pass1\n2fa: JBSWY3DPEHPK3PXP\n&\nlogin2:pass2</code>",
        reply_markup=cancel_kb(),
    )
    await callback.answer()


def _parse_stock_blocks(raw: str) -> list[str]:
    """Splits text into blocks by a line-separator '&'. Empty blocks are dropped."""
    blocks: list[str] = []
    current: list[str] = []
    for line in raw.replace("\r\n", "\n").split("\n"):
        if line.strip() == "&":
            block = "\n".join(current).strip()
            if block:
                blocks.append(block)
            current = []
        else:
            current.append(line)
    block = "\n".join(current).strip()
    if block:
        blocks.append(block)
    return blocks


@router.message(AdminStock.content)
async def add_stock_save(message: Message, state: FSMContext) -> None:
    if message.document:
        buffer = await message.bot.download(message.document)
        raw = buffer.read().decode("utf-8", errors="replace")
    else:
        raw = message.text or ""

    blocks = _parse_stock_blocks(raw)
    if not blocks:
        await message.answer("❌ Empty. Send text or a .txt file with positions.")
        return

    data = await state.get_data()
    await state.clear()
    with_2fa = 0
    async with get_session() as session:
        for block in blocks:
            secret = totp.extract_secret(block)
            if secret:
                with_2fa += 1
            session.add(
                Stock(item_id=data["item_id"], content=block, totp_secret=secret)
            )
        await session.commit()
    extra = f"\n🔐 Of them with 2FA: <b>{with_2fa}</b>" if with_2fa else ""
    await message.answer(
        f"✅ Stock positions added: <b>{len(blocks)}</b>.{extra}",
        reply_markup=back_admin_kb(),
    )


# ---------- Give balance ----------


@router.callback_query(F.data == "a:givebal")
async def give_bal_start(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    await state.set_state(AdminBalance.user_id)
    await callback.message.edit_text(
        "💵 Enter the user ID:", reply_markup=cancel_kb()
    )
    await callback.answer()


@router.message(AdminBalance.user_id)
async def give_bal_user(message: Message, state: FSMContext) -> None:
    text = (message.text or "").strip()
    if not text.isdigit():
        await message.answer("❌ The ID must be a number.")
        return
    await state.update_data(target_id=int(text))
    await state.set_state(AdminBalance.amount)
    await message.answer("Enter the amount (negative to deduct):")


@router.message(AdminBalance.amount)
async def give_bal_amount(message: Message, state: FSMContext) -> None:
    amount = parse_signed(message.text or "")
    if amount is None or amount == 0:
        await message.answer("❌ Enter a non-zero number (e.g. 5, -2 or 0.5).")
        return
    data = await state.get_data()
    await state.clear()
    settings = get_settings()
    async with get_session() as session:
        user = await session.get(User, data["target_id"])
        if user is None:
            await message.answer(
                "❌ User not found (they must start the bot at least once).",
                reply_markup=back_admin_kb(),
            )
            return
        user.balance = max(0.0, user.balance + amount)
        await session.commit()
        new_balance = user.balance
    await message.answer(
        f"✅ Done. New balance: <b>{money(new_balance)} {settings.currency}</b>.",
        reply_markup=back_admin_kb(),
    )
    try:
        await message.bot.send_message(
            data["target_id"],
            f"💰 Your balance was changed by an administrator by "
            f"{'+' if amount > 0 else '−'}{money(abs(amount))} {settings.currency}.\n"
            f"Current balance: {money(new_balance)} {settings.currency}.",
        )
    except Exception:
        pass


# ---------- Top-up review ----------


@router.callback_query(F.data.startswith("topok:"))
async def topup_approve(callback: CallbackQuery) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    topup_id = int(callback.data.split(":")[1])
    settings = get_settings()
    async with get_session() as session:
        topup = await session.get(TopUp, topup_id)
        if topup is None or topup.status != "pending":
            await callback.answer("Request already processed", show_alert=True)
            return
        topup.status = "approved"
        user = await session.get(User, topup.user_id)
        if user is None:
            await callback.answer("User not found", show_alert=True)
            return
        user.balance += topup.amount
        # referral bonus
        bonus = 0.0
        if user.referrer_id and settings.referral_percent > 0:
            referrer = await session.get(User, user.referrer_id)
            if referrer:
                bonus = round(topup.amount * settings.referral_percent / 100, 2)
                if bonus > 0:
                    referrer.balance += bonus
                    referrer.referral_earned += bonus
        await session.commit()
        amount = topup.amount
        target_id = topup.user_id
        new_balance = user.balance
        referrer_id = user.referrer_id

    await callback.message.edit_text(
        callback.message.text + f"\n\n✅ Approved ({money(new_balance)} {settings.currency})"
    )
    await callback.answer("Credited")
    try:
        await callback.bot.send_message(
            target_id,
            f"✅ Your top-up of <b>{money(amount)} {settings.currency}</b> has been credited!\n"
            f"💰 Balance: {money(new_balance)} {settings.currency}",
        )
    except Exception:
        pass
    if referrer_id and bonus > 0:
        try:
            await callback.bot.send_message(
                referrer_id,
                f"🎁 Referral bonus: +{money(bonus)} {settings.currency} "
                "for a top-up by an invited user!",
            )
        except Exception:
            pass


@router.callback_query(F.data.startswith("topno:"))
async def topup_decline(callback: CallbackQuery) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    topup_id = int(callback.data.split(":")[1])
    async with get_session() as session:
        topup = await session.get(TopUp, topup_id)
        if topup is None or topup.status != "pending":
            await callback.answer("Request already processed", show_alert=True)
            return
        topup.status = "declined"
        target_id = topup.user_id
        await session.commit()
    await callback.message.edit_text(callback.message.text + "\n\n❌ Declined")
    await callback.answer("Declined")
    try:
        await callback.bot.send_message(target_id, "❌ Your top-up request was declined.")
    except Exception:
        pass


# ---------- Broadcast ----------


@router.callback_query(F.data == "a:cast")
async def cast_start(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    await state.set_state(AdminBroadcast.message)
    await callback.message.edit_text(
        "📣 Send the broadcast message (text):", reply_markup=cancel_kb()
    )
    await callback.answer()


@router.message(AdminBroadcast.message)
async def cast_send(message: Message, state: FSMContext) -> None:
    await state.clear()
    text = message.text or ""
    async with get_session() as session:
        result = await session.execute(select(User.user_id).where(User.is_banned.is_(False)))
        user_ids = [row[0] for row in result.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=back_admin_kb(),
    )


# ---------- Settings (support / channels) ----------

_SET_FIELDS = {
    "support": (
        runtime.SUPPORT_URL,
        "⊙ <b>Support contact</b>\n\n"
        "Send a support link or username — e.g. <code>@my_support</code> or "
        "<code>https://t.me/my_support</code>.\nSend <code>-</code> to clear.",
    ),
    "reqchan": (
        runtime.REQUIRED_CHANNEL,
        "🔔 <b>Mandatory subscription channel</b>\n\n"
        "Users must join it before using the bot. Works for <b>private</b> channels too — "
        "the bot generates the “Join” link automatically.\n"
        "Send <code>@channel</code> or its numeric id <code>-100…</code>.\n"
        "⚠️ Add this bot as an <b>admin</b> (with the <i>Invite users</i> right for private "
        "channels), otherwise it can't check membership or make a join link.\n"
        "Send <code>-</code> to disable.",
    ),
    "revgroup": (
        runtime.REVIEWS_GROUP,
        "⭐ <b>Reviews group</b>\n\n"
        "New reviews will be posted here, and users get a “Reviews group” button in "
        "the “⋯ More” menu.\n"
        "Send <code>@group</code>, an invite link <code>https://t.me/…</code>, or its "
        "numeric id <code>-100…</code>.\n"
        "⚠️ Add this bot to the group so it can post. Note: the “More” button only shows "
        "for a public <code>@name</code> or an invite link (numeric ids have no link).\n"
        "Send <code>-</code> to disable.",
    ),
    "shopchan": (
        runtime.SHOP_CHANNEL,
        "🛍 <b>Shop channel</b>\n\n"
        "Promo posts (“Buy now”) are published here, and users get a “Channel” button in "
        "the “⋯ More” menu.\n"
        "Send <code>@channel</code>, an invite link <code>https://t.me/…</code>, or its "
        "numeric id <code>-100…</code>.\n"
        "⚠️ Add this bot as an <b>admin</b> so it can post. Note: the “More” button only "
        "shows for a public <code>@name</code> or an invite link (numeric ids have no link).\n"
        "Send <code>-</code> to disable.",
    ),
    "crypto": (
        runtime.CRYPTO_TOKEN,
        "💳 <b>CryptoBot payment token</b>\n\n"
        "Enables <b>automatic</b> balance top-ups: the bot creates a CryptoBot invoice and "
        "credits the balance once paid.\n"
        "Get the token in <a href=\"https://t.me/CryptoBot\">@CryptoBot</a> → Crypto Pay → "
        "Create App (or My Apps).\n"
        "Paste the API token here. Send <code>-</code> to disable (fall back to manual).",
    ),
    "asset": (
        runtime.CRYPTO_ASSET,
        "🪙 <b>Crypto asset</b>\n\n"
        "Which asset to charge, e.g. <code>USDT</code>, <code>TON</code>, <code>BTC</code>.\n"
        "Send <code>-</code> to reset to the default (USDT).",
    ),
}

# Fields whose value must be a chat reference (@name / link / numeric id).
_CHANNEL_FIELDS = {"reqchan", "revgroup", "shopchan"}


def _valid_chat_ref(value: str) -> bool:
    """Accepts @username, a t.me / http(s) link, or a numeric chat id."""
    if value.startswith("@") and len(value) > 1:
        return True
    if value.startswith(("http://", "https://", "t.me/", "telegram.me/")):
        return True
    if value.lstrip("-").isdigit():
        return True
    return False


@router.callback_query(F.data == "a:settings")
async def settings_open(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    await state.clear()
    await callback.message.edit_text(
        "⚙ <b>Settings</b>\n\nTap a field to change it:",
        reply_markup=settings_menu_kb(),
    )
    await callback.answer()


@router.callback_query(F.data.startswith("a:set:"))
async def settings_edit_start(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    field = callback.data.split(":")[2]
    if field not in _SET_FIELDS:
        await callback.answer()
        return
    _, prompt = _SET_FIELDS[field]
    await state.set_state(AdminSettings.value)
    await state.update_data(set_field=field)
    await callback.message.edit_text(prompt, reply_markup=cancel_kb())
    await callback.answer()


@router.message(AdminSettings.value)
async def settings_edit_save(message: Message, state: FSMContext) -> None:
    if not _admin_only(message.from_user.id):
        return
    data = await state.get_data()
    field = data.get("set_field", "")
    if field not in _SET_FIELDS:
        await state.clear()
        await message.answer("Session expired.", reply_markup=back_admin_kb())
        return
    key, _ = _SET_FIELDS[field]
    value = (message.text or "").strip()
    if value == "-":
        value = ""
    elif field in _CHANNEL_FIELDS and not _valid_chat_ref(value):
        await message.answer(
            "❌ Invalid. Send <code>@name</code>, a <code>https://t.me/…</code> link, "
            "or a numeric <code>-100…</code> id (or <code>-</code> to clear)."
        )
        return
    await state.clear()
    async with get_session() as session:
        await runtime.set_value(session, key, value)
    if key in (runtime.REQUIRED_CHANNEL, runtime.SHOP_CHANNEL, runtime.REVIEWS_GROUP):
        # drop cached invite links so the new chat resolves fresh
        from app.middlewares import clear_join_cache

        clear_join_cache()
    if field == "crypto":
        # never echo a secret token back into the chat
        shown = "set ✅ (automatic payments on)" if value else "not set (manual mode)"
    else:
        shown = value if value else "not set"
    await message.answer(
        f"✅ Saved: <code>{shown}</code>", reply_markup=settings_menu_kb()
    )


# ---------- Promo post ----------


@router.callback_query(F.data == "a:promo")
async def promo_start(callback: CallbackQuery, state: FSMContext) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    await state.clear()
    async with get_session() as session:
        result = await session.execute(
            select(Item).where(Item.is_active.is_(True)).order_by(Item.id)
        )
        items = result.scalars().all()
    if not items:
        await callback.message.edit_text(
            "No items to promote yet.", reply_markup=back_admin_kb()
        )
        await callback.answer()
        return
    rows = [(i.name, f"promo:item:{i.id}") for i in items]
    await callback.message.edit_text(
        "🖼 <b>Promo post</b>\n\nWhich item to promote?",
        reply_markup=pick_list_kb(rows),
    )
    await callback.answer()


async def _promo_text(item: Item, available: int, currency: str) -> str:
    desc = f"\n\n{item.description}" if item.description else ""
    return (
        f"🆕 <b>Just added — in stock now!</b>\n\n"
        f"🛒 <b>{item.name}</b>{desc}\n\n"
        f"💵 Price: <b>{money(item.price)} {currency}</b>\n"
        f"📦 Available: <b>{available} pcs.</b>"
    )


async def _deep_link(bot, item_id: int) -> str:
    me = await bot.me()
    return f"https://t.me/{me.username}?start=item{item_id}"


@router.callback_query(F.data.startswith("promo:item:"))
async def promo_preview(callback: CallbackQuery) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    settings = get_settings()
    item_id = int(callback.data.split(":")[2])
    async with get_session() as session:
        item = await session.get(Item, item_id)
        if item is None or not item.is_active:
            await callback.answer("Item unavailable", show_alert=True)
            return
        available = await _stock_count(session, item_id)
        text = await _promo_text(item, available, settings.currency)
    link = await _deep_link(callback.bot, item_id)
    await callback.message.answer(
        text, reply_markup=buy_now_kb(link)
    )
    target = runtime.get(runtime.SHOP_CHANNEL) or "— not set —"
    await callback.message.answer(
        f"👆 Preview. Publish to shop channel (<code>{target}</code>)?",
        reply_markup=promo_confirm_kb(item_id),
    )
    await callback.answer()


@router.callback_query(F.data.startswith("promo:pub:"))
async def promo_publish(callback: CallbackQuery) -> None:
    if not _admin_only(callback.from_user.id):
        await callback.answer()
        return
    settings = get_settings()
    item_id = int(callback.data.split(":")[2])
    channel = runtime.get(runtime.SHOP_CHANNEL)
    async with get_session() as session:
        item = await session.get(Item, item_id)
        if item is None or not item.is_active:
            await callback.answer("Item unavailable", show_alert=True)
            return
        available = await _stock_count(session, item_id)
        text = await _promo_text(item, available, settings.currency)
    link = await _deep_link(callback.bot, item_id)
    if not channel:
        await callback.message.edit_text(
            "⚠️ No shop channel set. Open ⚙ Settings → 🛍 Shop channel first "
            "(the preview above is ready to forward manually).",
            reply_markup=back_admin_kb(),
        )
        await callback.answer()
        return
    try:
        await callback.bot.send_message(channel, text, reply_markup=buy_now_kb(link))
    except Exception as exc:
        await callback.message.edit_text(
            f"❌ Couldn't publish to <code>{channel}</code>.\n"
            "Make sure the bot is an <b>admin</b> of that channel.\n"
            f"<code>{exc}</code>",
            reply_markup=back_admin_kb(),
        )
        await callback.answer()
        return
    await callback.message.edit_text(
        f"✅ Published to <code>{channel}</code>.", reply_markup=back_admin_kb()
    )
    await callback.answer("Published ✅")
