from __future__ import annotations

import asyncio
import logging
import re
from html import unescape

from aiogram import Bot, Dispatcher, F, Router
from aiogram.enums import ParseMode
from aiogram.filters import Command, CommandStart
from aiogram.fsm.context import FSMContext
from aiogram.fsm.strategy import FSMStrategy
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message, ReplyParameters
from aiogram.client.default import DefaultBotProperties

from config import Settings
from countries import ATTACK_TYPES, CAMPAIGN_TYPES, CONTINENTS, SECTIONS
from database import Database
from keyboards import (admin_kb, attack_kb, campaign_copy_kb, campaign_kb, campaign_news_kb,
                       confirmation_kb, continent_kb, country_kb, global_image_section_kb,
                       image_section_kb, login_kb, main_kb, kb)
from states import ActionStates, AdminStates, AttackStates, CampaignStates, LoginStates, TweetStates
from templates import ambush as ambush_template
from templates import ambush_admin_report
from templates import attack as attack_template
from templates import attack_admin_report
from templates import back as back_template
from templates import back_admin_report
from templates import campaign as campaign_template
from templates import campaign_admin_report
from templates import (ambush_confirmation, attack_confirmation, back_confirmation,
                       campaign_confirmation, tweet_confirmation)
from templates import tweet as tweet_template
from templates import tweet_admin_report

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("empire-bot")

settings = Settings.from_env()
db = Database(settings)
for owner_id in settings.admin_ids:
    db.add_admin(owner_id, None, None, protected=True)
router = Router()


def is_admin(user_id: int) -> bool:
    return db.is_admin(user_id)


def display_name(message: Message) -> str:
    user = message.from_user
    if not user:
        return "بدون نام"
    return (user.full_name or user.username or str(user.id)).strip()


def username(message: Message) -> str | None:
    return message.from_user.username if message.from_user else None


def configured_chat(key: str) -> int | str | None:
    raw = db.get_setting(key)
    if raw:
        return int(raw) if raw.lstrip("-").isdigit() else raw
    defaults = {
        "report_chat_id": settings.report_chat_id,
        "output_chat_id": settings.output_chat_id,
        "news_chat_id": settings.news_chat_id,
    }
    return defaults.get(key)


def image_key_for_country(country: str, section: str) -> str:
    """Country-specific image with a safe global fallback."""
    return f"country:{country}:{section}" if db.get_image(f"country:{country}:{section}") else f"global:{section}"


async def require_session(message: Message) -> str | None:
    row = db.get_session(message.from_user.id if message.from_user else 0)
    if not row:
        await message.answer("برای استفاده ابتدا با رمز کشور وارد شوید.", reply_markup=login_kb())
        return None
    return row["country"]


async def send_to_destination(
    bot: Bot,
    destination_key: str,
    text: str,
    image_key: str | None = None,
    *,
    reply_to_message_id: int | None = None,
    reply_markup: InlineKeyboardMarkup | None = None,
    chat_id_override: int | str | None = None,
) -> Message | None:
    # For replies, use the channel id stored with the original campaign.  If
    # an admin later changes the configured news destination, the reply must
    # still target the old post's chat, not the new channel.
    chat_id = chat_id_override if chat_id_override is not None else configured_chat(destination_key)
    if not chat_id:
        log.warning("No destination configured for %s", destination_key)
        return None
    file_id = db.get_image(image_key) if image_key else None
    kwargs = {"chat_id": chat_id, "parse_mode": ParseMode.HTML}
    if reply_to_message_id:
        kwargs["reply_parameters"] = ReplyParameters(message_id=int(reply_to_message_id))
    try:
        # Telegram captions are limited to 1024 characters. For a longer
        # report send the photo first and keep the complete text/action button
        # on a following message. An invalid/stale photo must never suppress
        # the operation text; fall back to text-only delivery.
        if file_id and len(text) <= 1024:
            try:
                return await bot.send_photo(
                    photo=file_id,
                    caption=text,
                    reply_markup=reply_markup,
                    **kwargs,
                )
            except Exception:
                log.warning("Image %s could not be sent; falling back to text", image_key, exc_info=True)
        elif file_id:
            try:
                await bot.send_photo(photo=file_id, **kwargs)
            except Exception:
                log.warning("Image %s could not be sent; falling back to text", image_key, exc_info=True)

        if len(text) <= 4096:
            return await bot.send_message(text=text, reply_markup=reply_markup, **kwargs)

        # Long user-entered stats can make a report exceed Telegram's message
        # limit. Use plain-text chunks so HTML tags/entities are never cut in
        # half. The action button belongs to the final chunk and its message id
        # becomes the canonical campaign post used by back/ambush replies.
        plain = unescape(re.sub(r"<[^>]+>", "", text))
        chunks = [plain[i:i + 4000] for i in range(0, len(plain), 4000)]
        sent = None
        for index, chunk in enumerate(chunks):
            sent = await bot.send_message(
                chat_id=chat_id,
                text=chunk,
                reply_parameters=kwargs.get("reply_parameters"),
                reply_markup=reply_markup if index == len(chunks) - 1 else None,
            )
        return sent
    except Exception:
        log.exception("Could not send message to %s", destination_key)
        return None


def destination_status(news: Message | None, report: Message | None) -> str:
    if news and report:
        return "📢 خبر عملیات و 📋 گزارش کامل ارسال شدند."
    missing = []
    if not news:
        missing.append("کانال خبر عملیات")
    if not report:
        missing.append("مقصد گزارش کامل")
    return "⚠️ ثبت شد؛ ارسال به " + " و ".join(missing) + " انجام نشد."


@router.message(CommandStart())
async def start(message: Message, state: FSMContext):
    await state.clear()
    row = db.get_session(message.from_user.id)
    if row:
        db.set_session(message.from_user.id, row["country"], display_name(message), username(message))
        await message.answer(f"🏳 کشور فعال شما: {row['country']}", reply_markup=main_kb(is_admin(message.from_user.id)))
    else:
        await message.answer("👋 به سامانه فرماندهی خوش آمدید. برای ورود، رمز کشور را وارد کنید.", reply_markup=login_kb())


@router.callback_query(F.data == "login")
async def login_button(call: CallbackQuery, state: FSMContext):
    await state.set_state(LoginStates.waiting_code)
    await call.message.answer("رمز کشور را ارسال کنید:")
    await call.answer()


@router.message(LoginStates.waiting_code)
async def login_code(message: Message, state: FSMContext):
    country = db.authenticate(message.text or "")
    if not country:
        await message.answer("رمز نادرست است یا برای هیچ کشوری تنظیم نشده. دوباره تلاش کنید:")
        return
    db.set_session(message.from_user.id, country, display_name(message), username(message))
    await state.clear()
    await message.answer(f"✅ ورود انجام شد.\n🏳 کشور شما: {country}", reply_markup=main_kb(is_admin(message.from_user.id)))


@router.callback_query(F.data == "logout")
async def logout(call: CallbackQuery, state: FSMContext):
    db.clear_session(call.from_user.id)
    await state.clear()
    await call.message.edit_text("از بات خارج شدید. برای ورود دوباره رمز کشور را وارد کنید.", reply_markup=login_kb())
    await call.answer()


@router.callback_query(F.data == "home")
async def home(call: CallbackQuery):
    if not db.get_session(call.from_user.id) and not is_admin(call.from_user.id):
        await call.message.edit_text("ابتدا وارد شوید.", reply_markup=login_kb())
    else:
        await call.message.edit_text("منوی اصلی:", reply_markup=main_kb(is_admin(call.from_user.id)))
    await call.answer()


@router.callback_query(F.data == "menu_campaign")
async def menu_campaign(call: CallbackQuery):
    if not db.get_session(call.from_user.id):
        await call.answer("ابتدا وارد شوید.", show_alert=True); return
    await call.message.edit_text("نوع لشکرکشی را انتخاب کنید:", reply_markup=campaign_kb()); await call.answer()


@router.callback_query(F.data.startswith("campaign:"))
async def campaign_start(call: CallbackQuery, state: FSMContext):
    kind = call.data.split(":", 1)[1]
    if kind not in CAMPAIGN_TYPES or not db.get_session(call.from_user.id):
        await call.answer("ابتدا وارد شوید.", show_alert=True); return
    await state.update_data(kind=kind)
    await state.set_state(CampaignStates.waiting_origin)
    await call.message.answer("مبدا حرکت را وارد کنید:")
    await call.answer()


@router.message(CampaignStates.waiting_origin)
async def campaign_origin(message: Message, state: FSMContext):
    await state.update_data(origin=message.text or "-")
    await state.set_state(CampaignStates.waiting_destination)
    await message.answer("مقصد حرکت را وارد کنید:")


@router.message(CampaignStates.waiting_destination)
async def campaign_destination(message: Message, state: FSMContext):
    await state.update_data(destination=message.text or "-")
    await state.set_state(CampaignStates.waiting_target_country)
    await message.answer("کشور مقصد را وارد کنید:")


@router.message(CampaignStates.waiting_target_country)
async def campaign_target_country(message: Message, state: FSMContext):
    await state.update_data(target_country=message.text or "-")
    await state.set_state(CampaignStates.waiting_stats)
    await message.answer("آمار لشکرکشی را وارد کنید:")


@router.message(CampaignStates.waiting_stats)
async def campaign_stats(message: Message, state: FSMContext):
    await state.update_data(stats=message.text or "-")
    await state.set_state(CampaignStates.waiting_eta)
    await message.answer("زمان رسیدن را به‌صورت متنی وارد کنید:")


@router.message(CampaignStates.waiting_eta)
async def campaign_eta(message: Message, state: FSMContext):
    await state.update_data(eta=message.text or "-")
    await state.set_state(CampaignStates.waiting_vanguard)
    await message.answer("متن/مشخصات پیش‌قراول را وارد کنید:")


@router.message(CampaignStates.waiting_vanguard)
async def campaign_finish(message: Message, state: FSMContext, bot: Bot):
    data = await state.get_data()
    session = db.get_session(message.from_user.id)
    if not session:
        await state.clear(); await message.answer("نشست شما منقضی شده است.", reply_markup=login_kb()); return
    data["vanguard"] = message.text or "-"
    data.update(country=session["country"], commander_name=display_name(message), sender_user_id=message.from_user.id)
    data.update(pending_operation="campaign", pending_user_id=message.from_user.id)
    await state.update_data(**data)
    await state.set_state(CampaignStates.waiting_confirmation)
    await message.answer(
        campaign_confirmation(
            data["kind"], data["country"], data["origin"], data["destination"],
            data["target_country"], data["eta"], data["commander_name"],
            data["stats"], data["vanguard"],
        ),
        reply_markup=confirmation_kb("campaign"),
    )


@router.callback_query(F.data == "menu_attack")
async def menu_attack(call: CallbackQuery):
    if not db.get_session(call.from_user.id):
        await call.answer("ابتدا وارد شوید.", show_alert=True); return
    await call.message.edit_text("نوع دستور را انتخاب کنید:", reply_markup=attack_kb()); await call.answer()


@router.callback_query(F.data.startswith("attack:"))
async def attack_start(call: CallbackQuery, state: FSMContext):
    kind = call.data.split(":", 1)[1]
    if kind not in ATTACK_TYPES or not db.get_session(call.from_user.id):
        await call.answer("ابتدا وارد شوید.", show_alert=True); return
    await state.update_data(kind=kind)
    await state.set_state(AttackStates.waiting_target_city)
    await call.message.answer("شهر یا بندر هدف را جداگانه وارد کنید:")
    await call.answer()


@router.message(AttackStates.waiting_target_city)
async def attack_target_city(message: Message, state: FSMContext):
    await state.update_data(target=(message.text or "-").strip() or "-")
    await state.set_state(AttackStates.waiting_target_country)
    await message.answer("قلمرو کشور هدف را جداگانه وارد کنید:")


@router.message(AttackStates.waiting_target)
async def attack_legacy_target(message: Message, state: FSMContext):
    """Recover an attack form that was open before the two-step target flow."""
    await attack_target_city(message, state)


@router.message(AttackStates.waiting_target_country)
async def attack_target_country(message: Message, state: FSMContext):
    await state.update_data(target_country=(message.text or "-").strip() or "-")
    await state.set_state(AttackStates.waiting_stats)
    await message.answer("آمار ارتش مهاجم را وارد کنید:")


@router.message(AttackStates.waiting_stats)
async def attack_finish(message: Message, state: FSMContext, bot: Bot):
    data = await state.get_data()
    session = db.get_session(message.from_user.id)
    if not session:
        await state.clear(); await message.answer("ابتدا وارد شوید.", reply_markup=login_kb()); return
    data.update(
        country=session["country"],
        commander_name=display_name(message),
        stats=message.text or "-",
        pending_operation="attack",
        pending_user_id=message.from_user.id,
    )
    await state.update_data(**data)
    await state.set_state(AttackStates.waiting_confirmation)
    await message.answer(
        attack_confirmation(
            data["kind"], data["country"], data["target"], data["target_country"],
            data["commander_name"], data["stats"],
        ),
        reply_markup=confirmation_kb("attack"),
    )


@router.callback_query(F.data == "menu_tweet")
async def menu_tweet(call: CallbackQuery, state: FSMContext):
    if not db.get_session(call.from_user.id):
        await call.answer("ابتدا وارد شوید.", show_alert=True); return
    await state.set_state(TweetStates.waiting_text)
    await call.message.answer("متن توییت را ارسال کنید:")
    await call.answer()


@router.message(TweetStates.waiting_text)
async def tweet_text(message: Message, state: FSMContext):
    await state.update_data(tweet_text=message.text or message.caption or "")
    await state.set_state(TweetStates.waiting_file)
    await message.answer("حالا فایل/موسیقی/فیلم/عکس را بفرستید. برای توییت متنی، عبارت «بدون فایل» را ارسال کنید:")


@router.message(TweetStates.waiting_file)
async def tweet_file(message: Message, state: FSMContext, bot: Bot):
    session = db.get_session(message.from_user.id)
    if not session:
        await state.clear(); await message.answer("ابتدا وارد شوید.", reply_markup=login_kb()); return
    data = await state.get_data()
    chat_id = configured_chat("output_chat_id")
    if not chat_id:
        await state.clear(); await message.answer("⚠️ مقصد خروجی توییت تنظیم نشده است.", reply_markup=main_kb(is_admin(message.from_user.id))); return
    media_type = "متن"
    media_file_id: str | None = None
    if (message.text or "").strip() == "بدون فایل":
        pass
    elif message.photo:
        media_type, media_file_id = "عکس", message.photo[-1].file_id
    elif message.video:
        media_type, media_file_id = "ویدئو", message.video.file_id
    elif message.audio:
        media_type, media_file_id = "موسیقی", message.audio.file_id
    elif message.document:
        media_type, media_file_id = "فایل", message.document.file_id
    elif message.voice:
        media_type, media_file_id = "ویس", message.voice.file_id
    else:
        await message.answer("یک فایل پشتیبانی‌شده بفرستید یا «بدون فایل» را ارسال کنید.")
        return

    data.update(
        media_type=media_type,
        media_file_id=media_file_id,
        pending_operation="tweet",
        pending_user_id=message.from_user.id,
        country=session["country"],
        commander_name=display_name(message),
    )
    await state.update_data(**data)
    await state.set_state(TweetStates.waiting_confirmation)
    await message.answer(
        tweet_confirmation(
            session["country"], data.get("tweet_text", ""), display_name(message), media_type
        ),
        reply_markup=confirmation_kb("tweet"),
    )


@router.callback_query(F.data.startswith("back:"))
async def back_start(call: CallbackQuery, state: FSMContext):
    try:
        campaign_id = int(call.data.split(":", 1)[1])
    except (TypeError, ValueError):
        await call.answer("شماره لشکرکشی نامعتبر است.", show_alert=True); return
    row = db.get_campaign(campaign_id)
    if not row or row["status"] != "active":
        await call.answer("این لشکرکشی فعال نیست.", show_alert=True); return
    if int(row["sender_user_id"]) != int(call.from_user.id):
        await call.answer("فقط صاحب همین لشکرکشی می‌تواند دستور بک بدهد.", show_alert=True); return
    await state.update_data(
        campaign_id=campaign_id,
        pending_user_id=call.from_user.id,
        reply_to_message_id=row["news_message_id"],
    )
    await state.set_state(ActionStates.waiting_time)
    await call.message.answer("زمان رسیدن به مبدا را وارد کنید:")
    await call.answer()


@router.message(ActionStates.waiting_time)
async def back_finish(message: Message, state: FSMContext, bot: Bot):
    data = await state.get_data()
    try:
        campaign_id = int(data["campaign_id"])
    except (KeyError, TypeError, ValueError):
        await state.clear()
        await message.answer("فرم بک منقضی شده است؛ دوباره از دکمه همان لشکرکشی وارد شوید.")
        return
    row = db.get_campaign(campaign_id)
    if not row or row["status"] != "active" or int(row["sender_user_id"]) != int(message.from_user.id):
        await state.clear(); return
    eta = message.text or "-"
    data.update(
        eta=eta,
        country=row["country"],
        pending_operation="back",
        pending_user_id=message.from_user.id,
    )
    await state.update_data(**data)
    await state.set_state(ActionStates.waiting_confirmation)
    await message.answer(
        back_confirmation(row["country"], row["origin"], row["destination"], eta, int(row["id"])),
        reply_markup=confirmation_kb("back"),
    )


@router.callback_query(F.data.startswith("ambush:"))
async def ambush_start(call: CallbackQuery, state: FSMContext, bot: Bot):
    try:
        campaign_id = int(call.data.split(":", 1)[1])
    except (TypeError, ValueError):
        await call.answer("شماره لشکرکشی نامعتبر است.", show_alert=True); return
    row = db.get_campaign(campaign_id)
    if not row or row["status"] != "active":
        await call.answer("این لشکرکشی فعال نیست.", show_alert=True); return
    session = db.get_session(call.from_user.id)
    if not session:
        await call.answer("برای کمین ابتدا وارد شوید.", show_alert=True); return
    # A player cannot ambush their own expedition.  Compare both the user id
    # and the active country: a second account using the same country password
    # must not be able to attack that country's own campaign either.
    if (int(row["sender_user_id"]) == int(call.from_user.id)
            or row["country"] == session["country"]):
        await call.answer("⚠️ شما نمی‌توانید به لشکرکشی خودتان کمین بزنید.", show_alert=True)
        return
    await state.update_data(
        campaign_id=campaign_id,
        pending_user_id=call.from_user.id,
        reply_to_message_id=row["news_message_id"],
    )
    await state.set_state(ActionStates.waiting_ambush_stats)
    # The button is under a channel post.  Never answer through
    # ``call.message.answer`` here, or the prompt would be sent to the public
    # news channel; ask the clicking player in their private chat instead.
    try:
        await bot.send_message(
            call.from_user.id,
            f"برای لشکرکشی شمارهٔ <b>#{campaign_id}</b> آمار خود را برای کمین وارد کنید:",
            parse_mode=ParseMode.HTML,
        )
    except Exception:
        # A user may have clicked the channel button without ever opening the
        # bot in a private chat.  Do not leave a half-open FSM state or let the
        # webhook fail; explain the one-time /start requirement on the button.
        log.exception("Could not open private ambush form for user %s", call.from_user.id)
        await state.clear()
        await call.answer("ابتدا ربات را در پیوی باز کنید و /start بزنید.", show_alert=True)
        return
    await call.answer("فرم کمین در پیوی شما ارسال شد.")


@router.message(ActionStates.waiting_ambush_stats)
@router.message(ActionStates.waiting_ambush_army)
async def ambush_finish(message: Message, state: FSMContext, bot: Bot):
    data = await state.get_data()
    try:
        campaign_id = int(data["campaign_id"])
    except (KeyError, TypeError, ValueError):
        await state.clear(); await message.answer("فرم کمین منقضی شده است."); return
    row = db.get_campaign(campaign_id)
    session = db.get_session(message.from_user.id)
    if not row or row["status"] != "active" or not session:
        await state.clear(); await message.answer("نشست یا لشکرکشی نامعتبر است."); return
    if (int(row["sender_user_id"]) == int(message.from_user.id)
            or row["country"] == session["country"]):
        await state.clear()
        await message.answer("⚠️ شما نمی‌توانید به لشکرکشی خودتان کمین بزنید.")
        return
    stats = (message.text or message.caption or "").strip() or "-"
    data.update(
        ambush_stats=stats,
        country=session["country"],
        commander_name=display_name(message),
        pending_operation="ambush",
        pending_user_id=message.from_user.id,
    )
    await state.update_data(**data)
    await state.set_state(ActionStates.waiting_confirmation)
    await message.answer(
        ambush_confirmation(
            session["country"], row["country"], row["origin"], row["destination"], stats
        ),
        reply_markup=confirmation_kb("ambush"),
    )


def _pending_matches(data: dict, operation: str, user_id: int) -> bool:
    return (
        data.get("pending_operation") == operation
        and int(data.get("pending_user_id", -1)) == int(user_id)
    )


async def _confirm_campaign(call: CallbackQuery, state: FSMContext, bot: Bot,
                            data: dict) -> None:
    session = db.get_session(call.from_user.id)
    if not session or int(data.get("sender_user_id", call.from_user.id)) != int(call.from_user.id):
        await state.clear()
        await call.message.edit_text("⚠️ نشست شما منقضی شده است؛ عملیات ثبت نشد.", reply_markup=login_kb())
        return

    # Create the row before sending anything so the campaign id can be bound
    # to the public news button.  The existing passwords/images are only read.
    campaign_id = db.add_campaign({
        **data,
        "country": session["country"],
        "sender_user_id": call.from_user.id,
        "news_chat_id": None,
        "news_message_id": None,
        "report_chat_id": None,
        "report_message_id": None,
        "private_chat_id": None,
        "private_message_id": None,
    })
    news_text = campaign_template(
        data["kind"], session["country"], data["origin"], data["destination"],
        data["target_country"], data["eta"], data["commander_name"],
        data["stats"], data["vanguard"],
    )
    report_text = campaign_admin_report(
        data["kind"], session["country"], data["origin"], data["destination"],
        data["target_country"], data["eta"], data["stats"], data["vanguard"],
    )
    image_key = image_key_for_country(session["country"], data["kind"])

    # News and report are independent network calls.  The news post carries
    # the campaign id so the ambush button is bound to this exact operation.
    news, report = await asyncio.gather(
        send_to_destination(
            bot, "news_chat_id", news_text, image_key,
            reply_markup=campaign_news_kb(campaign_id),
        ),
        send_to_destination(bot, "report_chat_id", report_text),
    )
    db.update_campaign(
        campaign_id,
        news_chat_id=news.chat.id if news else None,
        news_message_id=news.message_id if news else None,
        report_chat_id=report.chat.id if report else None,
        report_message_id=report.message_id if report else None,
    )

    # Consume the confirmation before the final UI sends so a duplicate
    # Telegram callback cannot create a second campaign.
    await state.clear()
    try:
        private = await call.message.answer(
            f"✅ لشکرکشی شماره <b>#{campaign_id}</b> ثبت شد.\n"
            "دکمهٔ بک این لشکرکشی در همین پیام قرار دارد.",
            reply_markup=campaign_copy_kb(campaign_id),
        )
        db.update_campaign(
            campaign_id,
            private_chat_id=private.chat.id,
            private_message_id=private.message_id,
        )
        await call.message.answer(
            destination_status(news, report),
            reply_markup=main_kb(is_admin(call.from_user.id)),
        )
    except Exception:
        log.exception("Campaign confirmation UI failed after campaign #%s was recorded", campaign_id)


async def _confirm_attack(call: CallbackQuery, state: FSMContext, bot: Bot,
                          data: dict) -> None:
    session = db.get_session(call.from_user.id)
    if not session:
        await state.clear()
        await call.message.edit_text("⚠️ نشست شما منقضی شده است؛ عملیات ثبت نشد.", reply_markup=login_kb())
        return
    news_text = attack_template(
        data["kind"], session["country"], data["target"], data["target_country"],
        data["commander_name"], data["stats"],
    )
    report_text = attack_admin_report(
        data["kind"], session["country"], data["target"], data["target_country"],
        data["stats"],
    )
    image_key = image_key_for_country(session["country"], data["kind"])
    await state.clear()
    news, report = await asyncio.gather(
        send_to_destination(bot, "news_chat_id", news_text, image_key),
        send_to_destination(bot, "report_chat_id", report_text),
    )
    await call.message.edit_text(
        "✅ دستور ثبت شد.\n" + destination_status(news, report),
        reply_markup=main_kb(is_admin(call.from_user.id)),
    )


async def _confirm_tweet(call: CallbackQuery, state: FSMContext, bot: Bot,
                         data: dict) -> None:
    session = db.get_session(call.from_user.id)
    chat_id = configured_chat("output_chat_id")
    if not session or not chat_id:
        await state.clear()
        await call.message.edit_text(
            "⚠️ نشست یا مقصد خروجی توییت تنظیم نشده است؛ عملیات ثبت نشد.",
            reply_markup=main_kb(is_admin(call.from_user.id)),
        )
        return

    text = tweet_template(session["country"], data.get("tweet_text", ""), data.get("commander_name", ""))
    media_type = data.get("media_type", "متن")
    media_file_id = data.get("media_file_id")
    try:
        if media_type == "متن":
            await bot.send_message(chat_id, text, parse_mode=ParseMode.HTML)
        elif media_type == "عکس":
            await bot.send_photo(chat_id, media_file_id, caption=text, parse_mode=ParseMode.HTML)
        elif media_type == "ویدئو":
            await bot.send_video(chat_id, media_file_id, caption=text, parse_mode=ParseMode.HTML)
        elif media_type == "موسیقی":
            await bot.send_audio(chat_id, media_file_id, caption=text, parse_mode=ParseMode.HTML)
        elif media_type == "فایل":
            await bot.send_document(chat_id, media_file_id, caption=text, parse_mode=ParseMode.HTML)
        elif media_type == "ویس":
            await bot.send_voice(chat_id, media_file_id, caption=text, parse_mode=ParseMode.HTML)
        else:
            raise ValueError(f"Unsupported tweet media type: {media_type}")
    except Exception:
        log.exception("Tweet send failed after confirmation")
        await state.clear()
        await call.message.edit_text(
            "⚠️ ارسال توییت ناموفق بود؛ دسترسی ربات به مقصد را بررسی کنید.",
            reply_markup=main_kb(is_admin(call.from_user.id)),
        )
        return

    report = await send_to_destination(
        bot,
        "report_chat_id",
        tweet_admin_report(session["country"], data.get("tweet_text", ""), media_type),
    )
    await state.clear()
    suffix = " گزارش ادمین هم ارسال شد." if report else " گزارش ادمین ارسال نشد."
    await call.message.edit_text(
        "✅ توییت به مقصد خروجی ارسال شد." + suffix,
        reply_markup=main_kb(is_admin(call.from_user.id)),
    )


async def _confirm_back(call: CallbackQuery, state: FSMContext, bot: Bot,
                        data: dict) -> None:
    try:
        campaign_id = int(data["campaign_id"])
    except (KeyError, TypeError, ValueError):
        await state.clear(); await call.message.edit_text("⚠️ لشکرکشی نامعتبر است.", reply_markup=main_kb(is_admin(call.from_user.id))); return
    row = db.get_campaign(campaign_id)
    if not row or int(row["sender_user_id"]) != int(call.from_user.id):
        await state.clear(); await call.message.edit_text("⚠️ این لشکرکشی در دسترس شما نیست.", reply_markup=main_kb(is_admin(call.from_user.id))); return
    if not db.claim_campaign(campaign_id, "active", "backed"):
        await state.clear(); await call.message.edit_text("⚠️ این لشکرکشی قبلاً بسته شده است.", reply_markup=main_kb(is_admin(call.from_user.id))); return

    text = back_template(row["country"], data.get("eta", "-"))
    image_key = image_key_for_country(row["country"], "back")
    await state.clear()
    news, report = await asyncio.gather(
        send_to_destination(
            bot, "news_chat_id", text, image_key,
            reply_to_message_id=row["news_message_id"],
            chat_id_override=row["news_chat_id"],
        ),
        send_to_destination(
            bot, "report_chat_id",
            back_admin_report(row["country"], row["origin"], row["destination"], data.get("eta", "-")),
        ),
    )
    if news and row["news_message_id"]:
        result_head = "✅ دستور بک ثبت شد و به پست اصلی لشکرکشی reply شد."
    elif news:
        result_head = "✅ دستور بک ثبت شد؛ پست اصلی این لشکرکشی قدیمی است و شناسهٔ reply آن ذخیره نشده بود."
    else:
        result_head = "⚠️ دستور بک در سیستم ثبت شد، اما ارسال به کانال خبر ناموفق بود."
    await call.message.edit_text(
        result_head + "\n" + destination_status(news, report),
        reply_markup=main_kb(is_admin(call.from_user.id)),
    )


async def _confirm_ambush(call: CallbackQuery, state: FSMContext, bot: Bot,
                          data: dict) -> None:
    try:
        campaign_id = int(data["campaign_id"])
    except (KeyError, TypeError, ValueError):
        await state.clear(); await call.message.edit_text("⚠️ لشکرکشی نامعتبر است.", reply_markup=main_kb(is_admin(call.from_user.id))); return
    row = db.get_campaign(campaign_id)
    session = db.get_session(call.from_user.id)
    if not row or not session:
        await state.clear(); await call.message.edit_text("⚠️ نشست یا لشکرکشی نامعتبر است.", reply_markup=main_kb(is_admin(call.from_user.id))); return
    if (int(row["sender_user_id"]) == int(call.from_user.id)
            or row["country"] == session["country"]):
        await state.clear()
        await call.message.edit_text(
            "⚠️ شما نمی‌توانید به لشکرکشی خودتان کمین بزنید.",
            reply_markup=main_kb(is_admin(call.from_user.id)),
        )
        return
    if not db.claim_campaign(campaign_id, "active", "ambushed"):
        await state.clear(); await call.message.edit_text("⚠️ این لشکرکشی قبلاً بسته شده است.", reply_markup=main_kb(is_admin(call.from_user.id))); return

    text = ambush_template(row["country"], session["country"], data.get("commander_name", ""))
    image_key = image_key_for_country(session["country"], "ambush")
    report_text = ambush_admin_report(
        session["country"], row["country"], row["origin"], row["destination"],
        data.get("ambush_stats", "-"),
    )
    await state.clear()
    news, report = await asyncio.gather(
        send_to_destination(
            bot, "news_chat_id", text, image_key,
            reply_to_message_id=row["news_message_id"],
            chat_id_override=row["news_chat_id"],
        ),
        send_to_destination(bot, "report_chat_id", report_text),
    )
    if news and row["news_message_id"]:
        result_head = "✅ کمین ثبت شد و به پست اصلی لشکرکشی reply شد."
    elif news:
        result_head = "✅ کمین ثبت شد؛ پست اصلی این لشکرکشی قدیمی است و شناسهٔ reply آن ذخیره نشده بود."
    else:
        result_head = "⚠️ کمین در سیستم ثبت شد، اما ارسال به کانال خبر ناموفق بود."
    await call.message.edit_text(
        result_head + "\n" + destination_status(news, report),
        reply_markup=main_kb(is_admin(call.from_user.id)),
    )


@router.callback_query(F.data.startswith("op_confirm:"))
async def operation_confirm(call: CallbackQuery, state: FSMContext, bot: Bot):
    operation = call.data.split(":", 1)[1]
    if operation not in {"campaign", "attack", "tweet", "back", "ambush"}:
        await call.answer("عملیات نامعتبر است.", show_alert=True); return
    data = await state.get_data()
    if not _pending_matches(data, operation, call.from_user.id):
        await call.answer("این فرم منقضی شده یا متعلق به شما نیست.", show_alert=True); return
    await call.answer("در حال ثبت نهایی...")
    if operation == "campaign":
        await _confirm_campaign(call, state, bot, data)
    elif operation == "attack":
        await _confirm_attack(call, state, bot, data)
    elif operation == "tweet":
        await _confirm_tweet(call, state, bot, data)
    elif operation == "back":
        await _confirm_back(call, state, bot, data)
    else:
        await _confirm_ambush(call, state, bot, data)


@router.callback_query(F.data.startswith("op_cancel:"))
async def operation_cancel(call: CallbackQuery, state: FSMContext):
    operation = call.data.split(":", 1)[1]
    data = await state.get_data()
    if operation not in {"campaign", "attack", "tweet", "back", "ambush"} or not _pending_matches(data, operation, call.from_user.id):
        await call.answer("این فرم منقضی شده یا متعلق به شما نیست.", show_alert=True); return
    await state.clear()
    await call.message.edit_text(
        "❌ عملیات لغو شد و هیچ پیامی ارسال نشد.",
        reply_markup=main_kb(is_admin(call.from_user.id)),
    )
    await call.answer("عملیات لغو شد.")


@router.callback_query(F.data == "admin")
async def admin_panel(call: CallbackQuery):
    if not is_admin(call.from_user.id):
        await call.answer("دسترسی ندارید.", show_alert=True); return
    await call.message.edit_text("پنل مدیریت:", reply_markup=admin_kb()); await call.answer()


@router.callback_query(F.data == "admin_codes")
async def admin_codes(call: CallbackQuery):
    if not is_admin(call.from_user.id): return
    await call.message.edit_text("قاره را انتخاب کنید:", reply_markup=continent_kb("admin_code")); await call.answer()


@router.callback_query(F.data.startswith("admin_code:continent:"))
async def admin_code_continent(call: CallbackQuery):
    if not is_admin(call.from_user.id): return
    try:
        idx = int(call.data.rsplit(":", 1)[1])
    except (TypeError, ValueError):
        await call.answer("قاره نامعتبر است؛ دوباره از پنل وارد شوید.", show_alert=True)
        return
    continents = list(CONTINENTS)
    if idx < 0 or idx >= len(continents):
        await call.answer("قاره نامعتبر است؛ دوباره از پنل وارد شوید.", show_alert=True)
        return
    continent = continents[idx]
    names = CONTINENTS[continent]
    rows = []
    for i in range(0, len(names), 2):
        row = [(names[i], f"admin_code_country:{idx}:{i}")]
        if i + 1 < len(names):
            row.append((names[i + 1], f"admin_code_country:{idx}:{i+1}"))
        rows.append(row)
    rows.append([("🔙 بازگشت", "admin_codes")])
    await call.message.edit_text(f"کشور از {continent}:", reply_markup=kb(rows)); await call.answer()


@router.callback_query(F.data.startswith("admin_code_country:"))
async def admin_code_country(call: CallbackQuery, state: FSMContext):
    if not is_admin(call.from_user.id): return
    # Country list is reconstructed from the previous menu index encoded in callback only; use visible list search by index over all countries.
    parts = call.data.split(":")
    if len(parts) != 3:
        await call.answer("کشور نامعتبر است؛ دوباره از پنل وارد شوید.", show_alert=True)
        return
    try:
        continent_index, country_index = int(parts[1]), int(parts[2])
        continents = list(CONTINENTS)
        if not (0 <= continent_index < len(continents)):
            raise ValueError
        if not (0 <= country_index < len(CONTINENTS[continents[continent_index]])):
            raise ValueError
    except (TypeError, ValueError):
        await call.answer("کشور نامعتبر است؛ دوباره از پنل وارد شوید.", show_alert=True)
        return
    await state.update_data(admin_continent_index=continent_index, admin_country_index=country_index)
    await state.set_state(AdminStates.waiting_code)
    await call.message.answer("رمز این کشور را ارسال کنید:")
    await call.answer()


@router.message(AdminStates.waiting_code)
async def admin_save_code(message: Message, state: FSMContext):
    if not is_admin(message.from_user.id): return
    data = await state.get_data(); cidx = int(data.get("admin_continent_index", -1)); idx = int(data.get("admin_country_index", -1))
    continents = list(CONTINENTS)
    names = CONTINENTS[continents[cidx]] if 0 <= cidx < len(continents) else []
    if 0 <= idx < len(names):
        try:
            db.set_code(names[idx], message.text or "")
        except ValueError:
            await message.answer("رمز نمی‌تواند خالی باشد؛ دوباره ارسال کنید:")
            return
        await message.answer(f"رمز کشور {names[idx]} ذخیره شد.", reply_markup=admin_kb())
    else:
        await message.answer("کشور نامعتبر است.", reply_markup=admin_kb())
    await state.clear()


@router.callback_query(F.data == "admin_images")
async def admin_images(call: CallbackQuery):
    if not is_admin(call.from_user.id): return
    await call.message.edit_text(
        "نوع تنظیم عکس را انتخاب کنید:",
        reply_markup=kb([[('🗺 عکس اختصاصی هر کشور', 'admin_images_country')],
                         [('🌐 عکس عمومی دستورات', 'admin_global_images')],
                         [('🔙 بازگشت', 'admin')]]),
    ); await call.answer()


@router.callback_query(F.data == "admin_images_country")
async def admin_images_country(call: CallbackQuery):
    if not is_admin(call.from_user.id): return
    await call.message.edit_text("برای تنظیم عکس اختصاصی، قاره را انتخاب کنید:", reply_markup=continent_kb("admin_img")); await call.answer()


@router.callback_query(F.data == "admin_global_images")
async def admin_global_images(call: CallbackQuery):
    if not is_admin(call.from_user.id): return
    await call.message.edit_text("بخش عمومی را انتخاب کنید:", reply_markup=global_image_section_kb()); await call.answer()


@router.callback_query(F.data.startswith("admin_global_section:"))
async def admin_global_section(call: CallbackQuery, state: FSMContext):
    if not is_admin(call.from_user.id): return
    section = call.data.split(":", 1)[1]
    if section not in SECTIONS:
        await call.answer("بخش عکس نامعتبر است؛ دوباره از پنل وارد شوید.", show_alert=True)
        return
    await state.update_data(admin_section=section)
    await state.set_state(AdminStates.waiting_global_photo)
    await call.message.answer(f"عکس عمومی بخش «{SECTIONS.get(section, section)}» را به‌صورت Photo ارسال کنید:")
    await call.answer()


@router.callback_query(F.data.startswith("admin_img:continent:"))
async def admin_img_continent(call: CallbackQuery, state: FSMContext):
    if not is_admin(call.from_user.id): return
    try:
        idx = int(call.data.rsplit(":", 1)[1])
    except (TypeError, ValueError):
        await call.answer("قاره نامعتبر است؛ دوباره از پنل عکس‌ها وارد شوید.", show_alert=True)
        return
    continents = list(CONTINENTS)
    if idx < 0 or idx >= len(continents):
        await call.answer("قاره نامعتبر است؛ دوباره از پنل عکس‌ها وارد شوید.", show_alert=True)
        return
    continent = continents[idx]
    await state.update_data(admin_continent=continent, admin_continent_index=idx)
    await call.message.edit_text(
        f"کشور از {continent} را انتخاب کنید:",
        reply_markup=country_kb(
            "admin_img_country",
            CONTINENTS[continent],
            "admin_images",
            callback_context=f"continent:{idx}:country",
        ),
    )
    await call.answer()


@router.callback_query(F.data.startswith("admin_img_country:"))
async def admin_img_country(call: CallbackQuery, state: FSMContext):
    if not is_admin(call.from_user.id): return
    # The old callback contained only a country index and depended on the
    # mutable FSM value ``admin_continent``.  An old/stale menu could therefore
    # resolve the same index against another continent.  Reject that shape and
    # require the new self-contained callback instead.
    parts = call.data.split(":")
    if len(parts) != 5 or parts[1] != "continent" or parts[3] != "country":
        await call.answer("این فهرست قدیمی است؛ دوباره قاره را انتخاب کنید.", show_alert=True)
        return
    try:
        continent_idx = int(parts[2])
        country_idx = int(parts[4])
    except (TypeError, ValueError):
        await call.answer("کشور نامعتبر است؛ دوباره از پنل عکس‌ها وارد شوید.", show_alert=True)
        return

    continents = list(CONTINENTS)
    if continent_idx < 0 or continent_idx >= len(continents):
        await call.answer("قاره نامعتبر است؛ دوباره از پنل عکس‌ها وارد شوید.", show_alert=True)
        return
    continent = continents[continent_idx]
    names = CONTINENTS[continent]
    if country_idx < 0 or country_idx >= len(names):
        await call.answer("کشور نامعتبر است؛ دوباره از پنل عکس‌ها وارد شوید.", show_alert=True)
        return

    country = names[country_idx]
    await state.update_data(
        admin_continent=continent,
        admin_continent_index=continent_idx,
        admin_country=country,
        admin_country_index=country_idx,
    )
    await call.message.edit_text(
        f"کشور انتخاب‌شده: <b>{country}</b>\nبخش مورد نظر را انتخاب کنید:",
        reply_markup=image_section_kb(continent_idx, country_idx),
    )
    await call.answer()


@router.callback_query(F.data.startswith("admin_img_section:"))
async def admin_img_section(call: CallbackQuery, state: FSMContext):
    if not is_admin(call.from_user.id): return
    # Country and continent are part of the callback, not inferred from a
    # possibly stale FSM state.  This prevents an image for Louisiana, for
    # example, being saved under New Spain after a stale/interleaved menu.
    parts = call.data.split(":")
    if len(parts) != 4:
        await call.answer("این فهرست قدیمی است؛ دوباره کشور را انتخاب کنید.", show_alert=True)
        return
    try:
        continent_idx = int(parts[1])
        country_idx = int(parts[2])
    except (TypeError, ValueError):
        await call.answer("انتخاب کشور نامعتبر است؛ دوباره تلاش کنید.", show_alert=True)
        return
    section = parts[3]
    continents = list(CONTINENTS)
    if continent_idx < 0 or continent_idx >= len(continents):
        await call.answer("قاره نامعتبر است؛ دوباره تلاش کنید.", show_alert=True)
        return
    continent = continents[continent_idx]
    names = CONTINENTS[continent]
    if country_idx < 0 or country_idx >= len(names) or section not in SECTIONS:
        await call.answer("انتخاب کشور یا بخش نامعتبر است؛ دوباره تلاش کنید.", show_alert=True)
        return
    country = names[country_idx]
    await state.update_data(
        admin_continent=continent,
        admin_continent_index=continent_idx,
        admin_country=country,
        admin_country_index=country_idx,
        admin_section=section,
    )
    await state.set_state(AdminStates.waiting_photo)
    await call.message.answer(
        f"کشور انتخاب‌شده: <b>{country}</b>\n"
        f"عکس بخش «{SECTIONS[section]}» را به‌صورت Photo ارسال کنید:"
    )
    await call.answer()


@router.message(AdminStates.waiting_photo, F.photo)
async def admin_save_photo(message: Message, state: FSMContext):
    if not is_admin(message.from_user.id): return
    data = await state.get_data(); country = data.get("admin_country"); section = data.get("admin_section")
    if not country or not section:
        await message.answer("انتخاب عکس منقضی شده است."); await state.clear(); return
    db.set_image(f"country:{country}:{section}", message.photo[-1].file_id)
    await state.clear(); await message.answer(f"عکس {SECTIONS.get(section, section)} برای {country} ذخیره شد.", reply_markup=admin_kb())


@router.message(AdminStates.waiting_global_photo, F.photo)
async def admin_save_global_photo(message: Message, state: FSMContext):
    if not is_admin(message.from_user.id): return
    data = await state.get_data(); section = data.get("admin_section")
    if not section:
        await message.answer("انتخاب عکس منقضی شده است."); await state.clear(); return
    db.set_image(f"global:{section}", message.photo[-1].file_id)
    await state.clear(); await message.answer(f"عکس عمومی {SECTIONS.get(section, section)} ذخیره شد.", reply_markup=admin_kb())


@router.callback_query(F.data.in_({"admin_report_chat", "admin_output_chat", "admin_news_chat"}))
async def admin_chat_setting(call: CallbackQuery, state: FSMContext):
    if not is_admin(call.from_user.id): return
    key = {
        "admin_report_chat": "report_chat_id",
        "admin_output_chat": "output_chat_id",
        "admin_news_chat": "news_chat_id",
    }[call.data]
    await state.update_data(chat_setting_key=key); await state.set_state(AdminStates.waiting_chat_id)
    await call.message.answer("📍 آیدی عددی یا یوزرنیم عمومی مقصد را ارسال کنید؛ مثال:\n-1001234567890\n@my_channel"); await call.answer()


def normalize_chat_ref(raw: str) -> int | str | None:
    raw = raw.strip()
    if re.fullmatch(r"-?\d+", raw):
        return int(raw)
    if re.fullmatch(r"@[A-Za-z0-9_]{5,}", raw):
        return raw
    return None


@router.message(AdminStates.waiting_chat_id)
async def admin_save_chat(message: Message, state: FSMContext, bot: Bot):
    if not is_admin(message.from_user.id): return
    data = await state.get_data(); key = data.get("chat_setting_key")
    chat_ref = normalize_chat_ref(message.text or "")
    if chat_ref is None:
        await message.answer("⚠️ مقدار نامعتبر است. آیدی عددی یا @username عمومی را دوباره بفرستید:")
        return
    try:
        chat = await bot.get_chat(chat_ref)
    except Exception:
        await message.answer("⚠️ مقصد پیدا نشد. یوزرنیم عمومی را بررسی کنید و مطمئن شوید ربات به مقصد دسترسی دارد:")
        return
    db.set_setting(key, str(chat_ref))
    await state.clear()
    await message.answer(f"✅ مقصد «{chat.title or chat_ref}» ذخیره شد. ربات را در چنل/گروه ادمین کنید.", reply_markup=admin_kb())


@router.callback_query(F.data == "admin_countries")
async def admin_countries(call: CallbackQuery):
    if not is_admin(call.from_user.id): return
    lines = []
    for continent, names in CONTINENTS.items():
        lines.append(f"🌍 {continent}")
        for n in names:
            row = db.row("SELECT code_hash FROM countries WHERE name=?", (n,))
            lines.append(f"• {n} {'✅' if row and row['code_hash'] else '⚪️'}")
    text = "فهرست کشورها و وضعیت رمز:\n\n" + "\n".join(lines)
    await call.message.edit_text(text[:4000], reply_markup=admin_kb()); await call.answer()


def admin_list_markup() -> InlineKeyboardMarkup:
    rows: list[list[tuple[str, str]]] = [[("➕ افزودن ادمین", "admin_add")]]
    for row in db.list_admins():
        label = f"❌ حذف {row['user_id']}"
        if row["username"]:
            label += f" @{row['username']}"
        if not row["protected"]:
            rows.append([(label, f"admin_remove:{row['user_id']}")])
    rows.append([("🔙 بازگشت", "admin")])
    return kb(rows)


def admin_list_text() -> str:
    lines = ["👥 <b>لیست ادمین‌های ربات</b>", ""]
    for row in db.list_admins():
        name = f"@{row['username']}" if row["username"] else "بدون username"
        badge = " 🔒" if row["protected"] else ""
        lines.append(f"• <code>{row['user_id']}</code> | {name}{badge}")
    if len(lines) == 2:
        lines.append("هنوز ادمین اضافه‌ای ثبت نشده است.")
    return "\n".join(lines)


@router.callback_query(F.data == "admin_list")
async def admin_list(call: CallbackQuery):
    if not is_admin(call.from_user.id): return
    await call.message.edit_text(admin_list_text(), reply_markup=admin_list_markup())
    await call.answer()


@router.callback_query(F.data == "admin_add")
async def admin_add(call: CallbackQuery, state: FSMContext):
    if not is_admin(call.from_user.id): return
    await state.set_state(AdminStates.waiting_admin_identity)
    await call.message.answer(
        "👤 آیدی عددی و در صورت وجود username را بفرستید؛ مثال:\n"
        "<code>123456789 @player_name</code>\n\n"
        "اگر فقط @username دارید، آن کاربر باید قبلاً حداقل یک‌بار /start زده باشد."
    )
    await call.answer()


@router.message(AdminStates.waiting_admin_identity)
async def admin_save_identity(message: Message, state: FSMContext, bot: Bot):
    if not is_admin(message.from_user.id): return
    raw = (message.text or "").strip()
    id_match = re.search(r"(?<!\d)(\d{3,})(?!\d)", raw)
    username_match = re.search(r"@[A-Za-z0-9_]{5,}", raw)
    user_id = int(id_match.group(1)) if id_match else None
    user_username = username_match.group(0).lstrip("@") if username_match else None

    # Also accept the common shorthand @123456789 for a numeric Telegram id.
    if user_id is None and user_username and user_username.isdigit():
        user_id = int(user_username)
        user_username = None

    if user_id is None and user_username:
        known = db.find_user_by_username(user_username)
        if known:
            user_id = int(known["user_id"])
        else:
            try:
                chat = await bot.get_chat("@" + user_username)
                user_id = int(chat.id)
            except Exception:
                await message.answer("⚠️ این username پیدا نشد؛ ابتدا کاربر /start بزند یا آیدی عددی را هم بفرستید:")
                return
    if user_id is None or user_id <= 0:
        await message.answer("⚠️ آیدی نامعتبر است. نمونه صحیح: <code>123456789 @player_name</code>")
        return

    known_session = db.get_session(user_id)
    if not user_username and known_session and known_session["username"]:
        user_username = known_session["username"]
    db.add_admin(user_id, user_username, message.from_user.id, protected=False)
    await state.clear()
    await message.answer(f"✅ کاربر <code>{user_id}</code> به لیست ادمین‌ها اضافه شد.", reply_markup=admin_kb())


@router.callback_query(F.data.startswith("admin_remove:"))
async def admin_remove(call: CallbackQuery):
    if not is_admin(call.from_user.id): return
    try:
        user_id = int(call.data.split(":", 1)[1])
    except ValueError:
        await call.answer("آیدی نامعتبر است.", show_alert=True); return
    if user_id == call.from_user.id:
        await call.answer("برای جلوگیری از قفل‌شدن پنل، خودتان را حذف نکنید.", show_alert=True); return
    if db.remove_admin(user_id):
        await call.answer("ادمین حذف شد.")
    else:
        await call.answer("این ادمین محافظت‌شده است یا وجود ندارد.", show_alert=True)
    await call.message.edit_text(admin_list_text(), reply_markup=admin_list_markup())


@router.message(Command("admin"))
async def admin_command(message: Message):
    if is_admin(message.from_user.id):
        await message.answer("پنل مدیریت:", reply_markup=admin_kb())


@router.message(Command("set_report_here"))
async def set_report_here(message: Message):
    if is_admin(message.from_user.id):
        db.set_setting("report_chat_id", str(message.chat.id)); await message.answer("این چت به‌عنوان مقصد گزارش ذخیره شد.")


@router.message(Command("set_output_here"))
async def set_output_here(message: Message):
    if is_admin(message.from_user.id):
        db.set_setting("output_chat_id", str(message.chat.id)); await message.answer("این چت به‌عنوان مقصد خروجی ذخیره شد.")


@router.message(Command("set_news_here"))
async def set_news_here(message: Message):
    if is_admin(message.from_user.id):
        db.set_setting("news_chat_id", str(message.chat.id)); await message.answer("این چت به‌عنوان کانال خبر عملیات ذخیره شد.")


def build_dispatcher() -> Dispatcher:
    # The ambush button is pressed on a channel post, while the form answer
    # arrives in the player's private chat.  GLOBAL_USER keeps both updates
    # in the same FSM context; the default USER_IN_CHAT would create two
    # separate states and make the private answer look like an unhandled text.
    dp = Dispatcher(fsm_strategy=FSMStrategy.GLOBAL_USER)
    dp.include_router(router)
    return dp


async def run_polling() -> None:
    """Local/terminal runner. cPanel uses passenger_wsgi.py webhook mode."""
    bot = Bot(settings.bot_token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
    dp = build_dispatcher()
    await bot.delete_webhook(drop_pending_updates=False)
    log.info("Bot started in long-polling debug mode")
    await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())


# Backward-compatible alias for old local commands/imports.
run = run_polling


if __name__ == "__main__":
    asyncio.run(run_polling())
