"""
ربات تلگرام برای اجرای تقسیم حجم روی MetaTrader 5.
جریان دکمه‌ای مرحله‌ای + تنظیمات ذخیره‌شده.
"""

from __future__ import annotations

import asyncio
import html
import logging
import os
import secrets
import sys
from datetime import datetime
from dotenv import load_dotenv
from telegram import Update
from telegram.constants import ParseMode
from telegram.error import BadRequest
from telegram.ext import (
    Application,
    CallbackQueryHandler,
    CommandHandler,
    ContextTypes,
    MessageHandler,
    filters,
)

from mt5_split import (
    account_confirm_block_html,
    account_mini_line_html,
    ensure_mt5,
    exposure_panel_html,
    mt5_connection_status_html,
    positions_pl_html,
    split_market_orders,
)
from settings_store import get_config, reset_config, update_config
from wizard_flow import (
    K_AWAIT,
    K_GO_NONCE,
    K_SIDE,
    K_STEP,
    K_SYMBOL,
    K_TOTAL,
    HAIR,
    HELP_HTML,
    SEP,
    TITLE,
    confirm_html,
    edit_or_reply,
    kb_confirm,
    kb_exposure_panel,
    kb_main,
    kb_settings_adv_menu,
    kb_settings_max_menu,
    kb_settings_risk_menu,
    kb_settings_root,
    kb_success_bar,
    kb_symbol,
    kb_totals,
    main_panel_html,
    parse_float_fa_en,
    parse_int_fa_en,
    parse_nonneg_float,
    parse_symbol_text,
    pick_symbol_html,
    pick_total_html,
    reset_flow,
    send_main_menu_message,
    settings_panel_html,
)

load_dotenv()

logging.basicConfig(
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
    level=logging.INFO,
)
log = logging.getLogger("tg_mt5")

# جلوگیری از اجرای همزمان split برای یک چت (دوبار زدن تأیید، …)
_trade_busy_chats: set[int] = set()
_trade_busy_lock = asyncio.Lock()

BUSY_TRADE_FA = (
    "⏳  <b>یک عملیات MT5 در این چت در جریان است</b>\n\n"
    "<i>صبر کن تا تمام شود — اجرای همزمان می‌تواند باعث حجم تکراری شود.</i>"
)


def _cfg_prompt(emoji: str, title: str, current: str, hint: str) -> str:
    """قالب یکدست برای صفحات ویرایش تنظیمات."""
    return (
        f"{TITLE}{SEP}"
        f"{emoji}  <b>{title}</b>\n\n"
        f"📍  الان  ·  <code>{current}</code>\n"
        f"💡  <i>{hint}</i>\n\n"
        f"✏️  مقدار جدید را بفرست…"
    )


async def _trade_busy_try_acquire(chat_id: int) -> bool:
    async with _trade_busy_lock:
        if chat_id in _trade_busy_chats:
            return False
        _trade_busy_chats.add(chat_id)
        return True


async def _trade_busy_release(chat_id: int) -> None:
    async with _trade_busy_lock:
        _trade_busy_chats.discard(chat_id)


# یک ترمینال MT5 = یک جریان order_send؛ صف کردن همهٔ چت‌ها جلوی قاطی شدن سفارش‌ها
_mt5_global_lock = asyncio.Lock()


def _parse_allowed_chats(raw: str) -> set[int]:
    out: set[int] = set()
    for part in raw.replace(" ", "").split(","):
        if not part:
            continue
        try:
            out.add(int(part))
        except ValueError:
            log.warning("چت‌آیدی نامعتبر نادیده گرفته شد: %s", part)
    return out


ALLOWED = _parse_allowed_chats(os.getenv("ALLOWED_CHAT_IDS", ""))
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
MT5_PATH = os.getenv("MT5_TERMINAL_PATH", "").strip() or None


def _trading_disabled() -> bool:
    v = os.getenv("TRADING_DISABLED", "").strip().lower()
    return v in ("1", "true", "yes", "on", "y")


def _settings_text(cid: int) -> str:
    c = get_config(cid)
    return settings_panel_html(
        c.default_symbol,
        c.default_max_lot_per_order,
        c.magic,
        c.deviation_points,
        c.comment,
        c.risk_max_total_lots,
        c.risk_max_split_orders,
        c.risk_margin_buffer_percent,
        c.risk_min_free_margin_percent_of_equity,
    )


async def _ui_symbol_step(chat_id: int, side: str, total: float) -> str:
    c = get_config(chat_id)

    def mini():
        return account_mini_line_html(MT5_PATH)

    strip = await asyncio.to_thread(mini)
    return pick_symbol_html(side, total, c.default_max_lot_per_order, strip)


async def _ui_confirm_step(
    chat_id: int,
    side: str,
    total: float,
    max_per: float,
    sym: str,
) -> str:
    def block():
        return account_confirm_block_html(sym, side, total, MT5_PATH)

    acc = await asyncio.to_thread(block)
    return confirm_html(side, total, max_per, sym, acc)


def _deny(update: Update) -> bool:
    cid = update.effective_chat.id if update.effective_chat else 0
    if ALLOWED and cid not in ALLOWED:
        log.warning("رد دسترسی chat_id=%s", cid)
        return True
    return False


def _parse_sym_magic(data: str, cid: int) -> tuple[str | None, int]:
    """callback_data به فرم 'w.xx|SYM' یا 'w.xx|SYM|MAGIC'.

    اگر MAGIC نباشد یا نامعتبر، magic فعلیِ پیکربندی این چت استفاده می‌شود.
    """
    parts = data.split("|")
    sym_raw = parts[1] if len(parts) > 1 else ""
    sym = parse_symbol_text(sym_raw)
    fallback_magic = get_config(cid).magic
    if len(parts) > 2 and parts[2]:
        try:
            return sym, int(parts[2])
        except ValueError:
            pass
    return sym, fallback_magic


async def _pl_html(sym: str, magic: int) -> str:
    """گزارش لحظه‌ای سود/ضرر با مهر زمان."""
    body = await asyncio.to_thread(positions_pl_html, sym, magic, MT5_PATH)
    ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
    return f"💹 <b>وضعیت لحظه‌ای پوزیشن‌ها</b> · <i>{ts}</i>\n\n{body}"


async def _exposure_html(sym: str, magic: int) -> str:
    """پنل حجم باز — فقط نمایش."""
    body = await asyncio.to_thread(exposure_panel_html, sym, magic, MT5_PATH)
    ts = datetime.now().strftime("%H:%M:%S")
    return f"👁  <b>حجم باز</b>  ·  <i>{ts}</i>\n\n{body}"


async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    if _deny(update):
        return
    reset_flow(context.user_data)
    cid = update.effective_chat.id
    c = get_config(cid)
    await send_main_menu_message(update, c.default_symbol, c.default_max_lot_per_order)


async def _mt5_status_html() -> str:
    body = await asyncio.to_thread(mt5_connection_status_html, MT5_PATH)
    return f"{TITLE}{SEP}📡  <b>وضعیت MT5</b>\n\n{body}"


def _risk_cap_html(c) -> str:
    if c.risk_max_total_lots <= 0:
        return "♾️  بدون سقف"
    return f"<code>{c.risk_max_total_lots:g}</code> لات"


async def _route_settings(
    q,
    ud: dict,
    data: str,
    cid: int,
) -> bool:
    """اگر True برگردد، مسیر تنظیمات بوده و پردازش تمام است."""
    c = get_config(cid)

    def show_root():
        return _settings_text(cid), kb_settings_root()

    if data == "w.cfg":
        reset_flow(ud)
        ud[K_AWAIT] = None
        txt, kb = show_root()
        await edit_or_reply(q, txt, kb)
        return True

    if data == "w.cfg.rst":
        reset_config(cid)
        txt, kb = show_root()
        await edit_or_reply(
            q,
            txt + f"{HAIR}<i>♻️  بازنشانی به مقادیر .env انجام شد.</i>",
            kb,
        )
        return True

    if data == "w.cfg.sym":
        ud[K_AWAIT] = "cfg_sym"
        await edit_or_reply(
            q,
            _cfg_prompt(
                "📌",
                "نماد پیش‌فرض",
                html.escape(c.default_symbol),
                "مثال: XAUUSD.p یا EURUSD.p",
            ),
            kb_settings_root(),
        )
        return True

    if data == "w.cfg.mg":
        ud[K_AWAIT] = "cfg_mg"
        await edit_or_reply(
            q,
            _cfg_prompt(
                "✨",
                "Magic",
                str(c.magic),
                "عدد صحیح (مثل اکسپرت MT5)",
            ),
            kb_settings_root(),
        )
        return True

    if data == "w.cfg.dv":
        ud[K_AWAIT] = "cfg_dv"
        await edit_or_reply(
            q,
            _cfg_prompt(
                "↔️",
                "انحراف (پوینت)",
                str(c.deviation_points),
                "عدد بین ۰ تا ۵۰۰",
            ),
            kb_settings_root(),
        )
        return True

    if data == "w.cfg.cm":
        ud[K_AWAIT] = "cfg_cm"
        cm_cur = html.escape((c.comment or "")[:120])
        if len(c.comment or "") > 120:
            cm_cur += "…"
        await edit_or_reply(
            q,
            _cfg_prompt(
                "💬",
                "کامنت سفارش",
                cm_cur or "—",
                "حداکثر ۳۱ کاراکتر MT5",
            ),
            kb_settings_root(),
        )
        return True

    if data == "w.cfg.mx":
        await edit_or_reply(
            q,
            f"{TITLE}{SEP}"
            f"✂️  <b>سقف لات هر سفارش</b>\n\n"
            f"📍  الان  ·  <code>{c.default_max_lot_per_order:g}</code> لات\n\n"
            f"<i>یک پریست را بزن یا عدد دلخواه بفرست.</i>",
            kb_settings_max_menu(),
        )
        return True

    if data.startswith("w.cfg.mx|"):
        sub = data[9:]
        if sub == "c":
            ud[K_AWAIT] = "cfg_ml"
            await edit_or_reply(
                q,
                _cfg_prompt(
                    "✂️",
                    "سقف لات هر سفارش",
                    f"{c.default_max_lot_per_order:g} لات",
                    "عدد مثبت — مثلاً 5 یا 0.5",
                ),
                kb_settings_max_menu(),
            )
            return True
        try:
            v = float(sub)
        except ValueError:
            return True
        if v <= 0:
            return True
        update_config(cid, default_max_lot_per_order=v)
        txt, kb = show_root()
        await edit_or_reply(q, txt, kb)
        return True

    if data == "w.cfg.rk":
        await edit_or_reply(
            q,
            f"{TITLE}{SEP}"
            f"📏  <b>سقف حجم کل هر اجرا</b>\n\n"
            f"📍  الان  ·  {_risk_cap_html(c)}\n\n"
            f"<i>یک پریست را بزن، عدد دلخواه بفرست، یا ۰ برای بدون سقف.</i>",
            kb_settings_risk_menu(),
        )
        return True

    if data.startswith("w.cfg.rk|"):
        sub = data[9:]
        if sub == "c":
            ud[K_AWAIT] = "cfg_rk"
            await edit_or_reply(
                q,
                _cfg_prompt(
                    "📏",
                    "سقف حجم کل",
                    _risk_cap_html(c),
                    "عدد لات یا 0 برای بدون سقف",
                ),
                kb_settings_risk_menu(),
            )
            return True
        try:
            v = float(sub)
        except ValueError:
            return True
        if v < 0:
            return True
        update_config(cid, risk_max_total_lots=v)
        txt, kb = show_root()
        await edit_or_reply(q, txt, kb)
        return True

    if data == "w.cfg.adv":
        await edit_or_reply(
            q,
            f"{TITLE}{SEP}"
            f"🛠  <b>ریسک پیشرفته</b>\n\n"
            f"📍  <b>مقادیر فعلی</b>\n"
            f"├ 🔢  حداکثر تعداد سفارش  ·  <code>{c.risk_max_split_orders}</code>\n"
            f"├ 📊  بافر مارجین  ·  <code>{c.risk_margin_buffer_percent:g}</code>٪\n"
            f"└ 💧  حداقل آزاد ٪ اکویتی  ·  <code>{c.risk_min_free_margin_percent_of_equity:g}</code>٪\n\n"
            f"<i>یکی از دکمه‌ها را برای تغییر بزن.</i>",
            kb_settings_adv_menu(),
        )
        return True

    if data.startswith("w.cfg.ord|"):
        try:
            n = int(data[10:])
        except ValueError:
            return True
        if n < 1:
            return True
        update_config(cid, risk_max_split_orders=n)
        txt, kb = show_root()
        await edit_or_reply(q, txt, kb)
        return True

    if data.startswith("w.cfg.buf|"):
        try:
            v = float(data[10:])
        except ValueError:
            return True
        if v < 0:
            return True
        update_config(cid, risk_margin_buffer_percent=v)
        txt, kb = show_root()
        await edit_or_reply(q, txt, kb)
        return True

    if data.startswith("w.cfg.mnf|"):
        try:
            v = float(data[10:])
        except ValueError:
            return True
        if v < 0:
            return True
        update_config(cid, risk_min_free_margin_percent_of_equity=v)
        txt, kb = show_root()
        await edit_or_reply(q, txt, kb)
        return True

    return False


async def on_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    if _deny(update):
        return
    q = update.callback_query
    if not q or not q.data:
        return
    data = q.data
    # برای «بروزرسانی» پاسخ callback را به انتهای همان بلوک می‌بریم
    # تا بتوانیم متن اعلان (toast) مناسب بفرستیم و فقط یک بار answer شود.
    if not data.startswith("w.pl|") and data != "w.ex":
        await q.answer()
    ud = context.user_data
    cid = q.message.chat_id if q.message else 0

    if data.startswith("w.pl|"):
        sym, magic = _parse_sym_magic(data, cid)
        if not sym:
            await q.answer("دادهٔ دکمه نامعتبر است.", show_alert=False)
            return
        text = await _pl_html(sym, magic)
        kb = kb_success_bar(sym, magic)
        try:
            await q.edit_message_text(
                text,
                reply_markup=kb,
                parse_mode=ParseMode.HTML,
            )
        except BadRequest as e:
            err = (str(e) or "").lower()
            # تلگرام وقتی متن/کیبورد با قبلی یکی باشد
            if "message is not modified" in err or "message_not_modified" in err:
                await q.answer("بدون تغییر نسبت به آخرین به‌روزرسانی.", show_alert=False)
                return
            # خطای پارس HTML — reply نزن (جلوگیری از اسپم پیام)
            if "parse" in err or "entity" in err:
                log.warning("w.pl HTML parse error: %s", e)
                await q.answer("خطا در قالب متن گزارش.", show_alert=True)
                return
            # پیام خیلی قدیمی یا غیرقابل ویرایش — حداکثر یک بار پیام جایگزین
            uneditable = (
                "message can't be edited" in err
                or "message to edit not found" in err
                or "message not found" in err
                or "have no rights" in err
            )
            if uneditable and q.message:
                mid = int(q.message.message_id)
                fb_key = ("w_pl_reply_fallback", cid, mid)
                if context.bot_data.get(fb_key):
                    await q.answer(
                        "این پیام دیگر قابل ویرایش نیست؛ گزارش جایگزین قبلاً فرستاده شد — در چت بالاتر بگرد.",
                        show_alert=False,
                    )
                    return
                try:
                    await q.message.reply_text(
                        text,
                        reply_markup=kb,
                        parse_mode=ParseMode.HTML,
                    )
                    context.bot_data[fb_key] = True
                    await q.answer("گزارش در یک پیام جدید فرستاده شد.", show_alert=False)
                except Exception as rep_e:
                    log.warning("w.pl reply fallback failed: %s", rep_e)
                    await q.answer("ارسال گزارش جایگزین ناموفق بود.", show_alert=True)
                return
            log.debug("w.pl edit BadRequest: %s", e)
            await q.answer("به‌روزرسانی روی همین پیام ناموفق بود.", show_alert=False)
            return
        except Exception as e:
            log.warning("w.pl edit unexpected error: %s", e, exc_info=True)
            await q.answer("خطای غیرمنتظره هنگام به‌روزرسانی.", show_alert=True)
            return
        await q.answer("به‌روز شد.", show_alert=False)
        return

    if data == "w.ex":
        c = get_config(cid)
        text = await _exposure_html(c.default_symbol, c.magic)
        kb = kb_exposure_panel()
        try:
            await q.edit_message_text(
                text,
                reply_markup=kb,
                parse_mode=ParseMode.HTML,
            )
        except BadRequest as e:
            err = (str(e) or "").lower()
            if "message is not modified" in err or "message_not_modified" in err:
                await q.answer("بدون تغییر نسبت به آخرین به‌روزرسانی.", show_alert=False)
                return
            if "parse" in err or "entity" in err:
                log.warning("w.ex HTML parse error: %s", e)
                await q.answer("خطا در قالب متن گزارش.", show_alert=True)
                return
            uneditable = (
                "message can't be edited" in err
                or "message to edit not found" in err
                or "message not found" in err
                or "have no rights" in err
            )
            if uneditable and q.message:
                mid = int(q.message.message_id)
                fb_key = ("w_ex_reply_fallback", cid, mid)
                if context.bot_data.get(fb_key):
                    await q.answer(
                        "این پیام دیگر قابل ویرایش نیست؛ گزارش جایگزین قبلاً فرستاده شد.",
                        show_alert=False,
                    )
                    return
                try:
                    await q.message.reply_text(
                        text,
                        reply_markup=kb,
                        parse_mode=ParseMode.HTML,
                    )
                    context.bot_data[fb_key] = True
                    await q.answer("گزارش در یک پیام جدید فرستاده شد.", show_alert=False)
                except Exception as rep_e:
                    log.warning("w.ex reply fallback failed: %s", rep_e)
                    await q.answer("ارسال گزارش جایگزین ناموفق بود.", show_alert=True)
                return
            log.debug("w.ex edit BadRequest: %s", e)
            await q.answer("به‌روزرسانی روی همین پیام ناموفق بود.", show_alert=False)
            return
        except Exception as e:
            log.warning("w.ex edit unexpected error: %s", e, exc_info=True)
            await q.answer("خطای غیرمنتظره هنگام به‌روزرسانی.", show_alert=True)
            return
        await q.answer("به‌روز شد.", show_alert=False)
        return

    if data.startswith("w.cfg"):
        handled = await _route_settings(q, ud, data, cid)
        if handled:
            return

    c = get_config(cid)

    if data == "w.m":
        reset_flow(ud)
        await edit_or_reply(
            q,
            main_panel_html(c.default_symbol, c.default_max_lot_per_order),
            kb_main(),
        )
        return

    if data == "w.mn":
        # پیام فعلی را نگه می‌داریم و یک پیام جدید برای منو می‌فرستیم.
        reset_flow(ud)
        if q.message:
            try:
                await q.message.reply_text(
                    main_panel_html(c.default_symbol, c.default_max_lot_per_order),
                    reply_markup=kb_main(),
                    parse_mode=ParseMode.HTML,
                )
            except Exception:
                pass
        return

    if data == "w.st":
        html_txt = await _mt5_status_html()
        await edit_or_reply(q, html_txt, kb_main())
        return

    if data == "w.h":
        await edit_or_reply(q, HELP_HTML, kb_main())
        return

    if data in ("w.s.b", "w.s.s"):
        ud[K_SIDE] = "buy" if data.endswith(".b") else "sell"
        ud[K_STEP] = "total"
        ud[K_AWAIT] = None
        side = ud[K_SIDE]
        await edit_or_reply(q, pick_total_html(side), kb_totals())
        return

    if data.startswith("w.t."):
        if not ud.get(K_SIDE):
            await edit_or_reply(
                q,
                main_panel_html(c.default_symbol, c.default_max_lot_per_order),
                kb_main(),
            )
            return
        sub = data[4:]
        if sub == "c":
            ud[K_AWAIT] = "total"
            await edit_or_reply(
                q,
                pick_total_html(ud[K_SIDE])
                + f"{HAIR}✏️  حجم کل را بفرست  ·  مثلاً <code>350</code>",
                kb_totals(),
            )
            return
        try:
            total = float(sub)
        except ValueError:
            return
        if total <= 0:
            return
        if c.risk_max_total_lots > 0 and total > c.risk_max_total_lots:
            await edit_or_reply(
                q,
                pick_total_html(ud[K_SIDE])
                + (
                    f"{HAIR}🛑  <b>توقف ایمنی</b>\n"
                    f"<code>{total:g}</code> لات بزرگ‌تر از سقف ریسک "
                    f"<code>{c.risk_max_total_lots:g}</code> است.\n"
                    f"<i>از تنظیمات سقف را بالا ببر یا حجم کمتر بزن.</i>"
                ),
                kb_totals(),
            )
            return
        ud[K_TOTAL] = total
        ud[K_STEP] = "sym"
        ud[K_AWAIT] = None
        sym_txt = await _ui_symbol_step(cid, ud[K_SIDE], total)
        await edit_or_reply(q, sym_txt, kb_symbol(c.default_symbol))
        return

    if data == "w.b.t":
        if not ud.get(K_SIDE):
            await edit_or_reply(
                q,
                main_panel_html(c.default_symbol, c.default_max_lot_per_order),
                kb_main(),
            )
            return
        ud.pop(K_TOTAL, None)
        ud.pop(K_SYMBOL, None)
        ud[K_AWAIT] = None
        ud[K_STEP] = "total"
        await edit_or_reply(q, pick_total_html(ud[K_SIDE]), kb_totals())
        return

    if data == "w.y.d":
        if K_TOTAL not in ud or not ud.get(K_SIDE):
            await edit_or_reply(
                q,
                main_panel_html(c.default_symbol, c.default_max_lot_per_order),
                kb_main(),
            )
            return
        ud[K_SYMBOL] = c.default_symbol
        ud[K_STEP] = "confirm"
        ud[K_AWAIT] = None
        ud[K_GO_NONCE] = secrets.token_hex(8)
        c_txt = await _ui_confirm_step(
            cid,
            ud[K_SIDE],
            float(ud[K_TOTAL]),
            c.default_max_lot_per_order,
            ud[K_SYMBOL],
        )
        await edit_or_reply(q, c_txt, kb_confirm(ud[K_GO_NONCE]))
        return

    if data == "w.y.c":
        if K_TOTAL not in ud or not ud.get(K_SIDE):
            await edit_or_reply(
                q,
                main_panel_html(c.default_symbol, c.default_max_lot_per_order),
                kb_main(),
            )
            return
        ud[K_AWAIT] = "sym"
        sym_txt = await _ui_symbol_step(cid, ud[K_SIDE], float(ud[K_TOTAL]))
        await edit_or_reply(
            q,
            sym_txt + (
                f"{HAIR}✏️  نماد را بفرست  ·  "
                f"مثلاً <code>{html.escape(c.default_symbol)}</code>"
            ),
            kb_symbol(c.default_symbol),
        )
        return

    if data == "w.b.y":
        if K_TOTAL not in ud or not ud.get(K_SIDE):
            await edit_or_reply(
                q,
                main_panel_html(c.default_symbol, c.default_max_lot_per_order),
                kb_main(),
            )
            return
        ud.pop(K_SYMBOL, None)
        ud[K_STEP] = "sym"
        ud[K_AWAIT] = None
        sym_txt = await _ui_symbol_step(cid, ud[K_SIDE], float(ud[K_TOTAL]))
        await edit_or_reply(q, sym_txt, kb_symbol(c.default_symbol))
        return

    if data == "w.rs":
        reset_flow(ud)
        await edit_or_reply(
            q,
            main_panel_html(c.default_symbol, c.default_max_lot_per_order),
            kb_main(),
        )
        return

    if data.startswith("w.go"):
        nonce_cb = ""
        if data.startswith("w.go|"):
            nonce_cb = data[5:]
        if not nonce_cb:
            try:
                await q.answer(
                    "این دکمهٔ تأیید منقضی است — از منو دوباره جریان را بساز.",
                    show_alert=True,
                )
            except Exception:
                pass
            return

        side = ud.get(K_SIDE)
        sym = ud.get(K_SYMBOL)
        if side not in ("buy", "sell") or sym is None or K_TOTAL not in ud:
            try:
                await q.answer("جریان ناقص است — از منو شروع کن.", show_alert=True)
            except Exception:
                pass
            return

        if not await _trade_busy_try_acquire(cid):
            log.warning("trade busy; w.go ignored chat_id=%s", cid)
            if q.message:
                await q.message.reply_text(BUSY_TRADE_FA, parse_mode=ParseMode.HTML)
            try:
                await q.answer()
            except Exception:
                pass
            return

        try:
            if ud.get(K_GO_NONCE) != nonce_cb:
                try:
                    await q.answer(
                        "جلسهٔ تأیید نامعتبر یا منقضی — صفحهٔ تأیید را دوباره بساز.",
                        show_alert=True,
                    )
                except Exception:
                    pass
                return

            if _trading_disabled():
                try:
                    await q.answer("معامله از سوی مدیر خاموش است (TRADING_DISABLED).", show_alert=True)
                except Exception:
                    pass
                return

            ud.pop(K_GO_NONCE, None)

            try:
                await q.answer()
            except Exception:
                pass

            # بلافاصله دکمه‌ها را برمی‌داریم تا با چندبار زدن (صف تلگرام / UI گیرکرده)
            # همان callback دوباره «تأیید» نشود و کاربر حس گیرکردن نگیرد.
            try:
                await q.edit_message_reply_markup(reply_markup=None)
            except Exception:
                pass

            total = float(ud[K_TOTAL])
            cfg = get_config(cid)
            max_per = cfg.default_max_lot_per_order
            confirm_full = await _ui_confirm_step(cid, side, total, max_per, sym)
            await edit_or_reply(
                q,
                confirm_full + f"{HAIR}⏳  <b>در حال ارسال سفارش‌ها…</b>",
                None,
            )

            def work():
                ensure_mt5(MT5_PATH)
                return split_market_orders(
                    symbol=sym,
                    total_lots=total,
                    max_lot_per_order=max_per,
                    side=side,  # type: ignore[arg-type]
                    deviation=cfg.deviation_points,
                    magic=cfg.magic,
                    comment=cfg.comment,
                    terminal_path=MT5_PATH,
                    risk_tuple=cfg.risk_tuple(),
                )

            try:
                async with _mt5_global_lock:
                    result = await asyncio.to_thread(work)
            except Exception as e:
                log.exception("split_market_orders failed chat_id=%s", cid)
                err_msg = html.escape(str(e) if str(e) else type(e).__name__)
                result = None
                try:
                    if q.message:
                        await q.message.reply_text(
                            f"❌  <b>خطای داخلی</b>\n<code>{err_msg}</code>",
                            parse_mode=ParseMode.HTML,
                        )
                except Exception:
                    pass
            else:
                try:
                    if not q.message:
                        pass
                    elif result.ok:
                        # پیام موفقیت + گزارش سود/ضرر لحظه‌ای + دکمه‌ها
                        pl_block = await _pl_html(sym, cfg.magic)
                        await q.message.reply_text(
                            f"🎉  {result.message}{HAIR}{pl_block}",
                            reply_markup=kb_success_bar(sym, cfg.magic),
                            parse_mode=ParseMode.HTML,
                        )
                    else:
                        await q.message.reply_text(
                            f"❌  <b>اجرا ناموفق بود</b>\n\n{result.message}",
                            parse_mode=ParseMode.HTML,
                        )
                except Exception:
                    pass
            # پیام نتیجه دکمه‌ی «منو» دارد؛ نیازی به ارسال جداگانه‌ی منو نیست.
            reset_flow(ud)
        finally:
            await _trade_busy_release(cid)
        return


async def on_text_wizard(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    if _deny(update) or not update.message or not update.message.text:
        return
    ud = context.user_data
    cid = update.effective_chat.id
    aw = ud.get(K_AWAIT)
    raw = update.message.text.strip()
    c = get_config(cid)

    if aw in ("cfg_sym", "cfg_mg", "cfg_dv", "cfg_cm", "cfg_ml", "cfg_rk"):
        if aw == "cfg_sym":
            sym = parse_symbol_text(raw)
            if not sym:
                await update.message.reply_text(
                    "⚠️  <b>نماد نامعتبر</b>\n<i>فقط حروف/اعداد استاندارد MT5.</i>",
                    parse_mode=ParseMode.HTML,
                )
                return
            update_config(cid, default_symbol=sym)
        elif aw == "cfg_mg":
            mg = parse_int_fa_en(raw)
            if mg is None or mg < 1 or mg > 2147483647:
                await update.message.reply_text(
                    "⚠️  <b>عدد نامعتبر</b>\n<i>عدد صحیح بین ۱ و 2,147,483,647.</i>",
                    parse_mode=ParseMode.HTML,
                )
                return
            update_config(cid, magic=mg)
        elif aw == "cfg_dv":
            dv = parse_int_fa_en(raw)
            if dv is None or dv < 0 or dv > 500:
                await update.message.reply_text(
                    "⚠️  <b>خارج از محدوده</b>\n<i>عدد بین ۰ تا ۵۰۰.</i>",
                    parse_mode=ParseMode.HTML,
                )
                return
            update_config(cid, deviation_points=dv)
        elif aw == "cfg_cm":
            update_config(cid, comment=raw[:31])
        elif aw == "cfg_ml":
            v = parse_float_fa_en(raw)
            if v is None:
                await update.message.reply_text(
                    "⚠️  <b>عدد نامعتبر</b>\n<i>یک عدد مثبت بفرست — مثلاً 5 یا 0.5.</i>",
                    parse_mode=ParseMode.HTML,
                )
                return
            update_config(cid, default_max_lot_per_order=float(v))
        elif aw == "cfg_rk":
            v = parse_nonneg_float(raw)
            if v is None:
                await update.message.reply_text(
                    "⚠️  <b>عدد نامعتبر</b>\n<i>عدد ≥ ۰ (۰ = ♾️ بدون سقف).</i>",
                    parse_mode=ParseMode.HTML,
                )
                return
            update_config(cid, risk_max_total_lots=float(v))
        ud[K_AWAIT] = None
        await update.message.reply_text(
            _settings_text(cid) + f"{HAIR}<i>✅  ذخیره شد.</i>",
            reply_markup=kb_settings_root(),
            parse_mode=ParseMode.HTML,
        )
        return

    if aw not in ("total", "sym"):
        return

    if aw == "total":
        v = parse_float_fa_en(raw)
        if v is None:
            await update.message.reply_text(
                "⚠️  <b>عدد نامعتبر</b>\n<i>یک عدد مثبت بفرست — مثلاً 350 یا 0.5.</i>",
                parse_mode=ParseMode.HTML,
            )
            return
        if c.risk_max_total_lots > 0 and v > c.risk_max_total_lots:
            await update.message.reply_text(
                (
                    f"🛑  <b>توقف ایمنی</b>\n"
                    f"<code>{v:g}</code> لات بزرگ‌تر از سقف ریسک "
                    f"<code>{c.risk_max_total_lots:g}</code> است.\n"
                    f"<i>از تنظیمات سقف را بالا ببر یا حجم کمتر بزن.</i>"
                ),
                parse_mode=ParseMode.HTML,
            )
            return
        ud[K_TOTAL] = float(v)
        ud[K_AWAIT] = None
        ud[K_STEP] = "sym"
        sym_body = await _ui_symbol_step(cid, ud[K_SIDE], float(ud[K_TOTAL]))
        await update.message.reply_text(
            sym_body,
            reply_markup=kb_symbol(c.default_symbol),
            parse_mode=ParseMode.HTML,
        )
        return

    if aw == "sym":
        sym = parse_symbol_text(raw)
        if sym is None:
            await update.message.reply_text(
                "⚠️  <b>نماد نامعتبر</b>\n<i>فقط حروف/اعداد استاندارد MT5.</i>",
                parse_mode=ParseMode.HTML,
            )
            return
        ud[K_SYMBOL] = sym
        ud[K_AWAIT] = None
        ud[K_STEP] = "confirm"
        ud[K_GO_NONCE] = secrets.token_hex(8)
        c_body = await _ui_confirm_step(
            cid,
            ud[K_SIDE],
            float(ud[K_TOTAL]),
            c.default_max_lot_per_order,
            sym,
        )
        await update.message.reply_text(
            c_body,
            reply_markup=kb_confirm(ud[K_GO_NONCE]),
            parse_mode=ParseMode.HTML,
        )


def main() -> None:
    if not TOKEN:
        log.error("TELEGRAM_BOT_TOKEN در .env نیست.")
        sys.exit(1)
    if not ALLOWED:
        log.error("ALLOWED_CHAT_IDS خالی است — برای امنیت حداقل یک چت‌آیدی بگذار.")
        sys.exit(1)

    if _trading_disabled():
        log.warning(
            "TRADING_DISABLED فعال است — دکمهٔ «تأیید و اجرا» به کاربر پیام می‌دهد ولی سفارش ارسال نمی‌شود."
        )
    for _lk in ("MT5_EXPECT_LOGIN", "SAFETY_MT5_EXPECTED_LOGIN"):
        _lv = os.getenv(_lk, "").strip()
        if _lv:
            log.info("ایمنی حساب MT5: فقط لاگین %s (کلید %s در .env).", _lv, _lk)
            break
    _cap = os.getenv("SAFETY_ABSOLUTE_MAX_EXECUTE_LOTS", "").strip()
    if _cap:
        log.info(
            "ایمنی حجم: سقف مطلق هر اجرا %s لات (SAFETY_ABSOLUTE_MAX_EXECUTE_LOTS).",
            _cap,
        )
    _sp = os.getenv("SAFETY_MAX_SPREAD_POINTS", "").strip()
    if _sp:
        log.info("ایمنی اسپرد: حداکثر %s پوینت (SAFETY_MAX_SPREAD_POINTS).", _sp)

    app = Application.builder().token(TOKEN).build()
    app.add_handler(CommandHandler("start", cmd_start))
    app.add_handler(CallbackQueryHandler(on_callback, pattern=r"^w\."))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, on_text_wizard))

    async def on_error(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
        log.exception("خطای پردازش آپدیت", exc_info=context.error)

    app.add_error_handler(on_error)

    log.info("ربات روشن شد (دکمه‌ای + تنظیمات).")
    app.run_polling(allowed_updates=Update.ALL_TYPES)


if __name__ == "__main__":
    main()
