"""
منطق تقسیم حجم و ارسال سفارش مارکت از طریق API پایتون متاتریدر 5.
ترمینال MT5 باید باز و به حساب وصل باشد.
"""

from __future__ import annotations

import html
import math
import os
import threading
from dataclasses import dataclass
from typing import Literal

import MetaTrader5 as mt5

OrderSide = Literal["buy", "sell"]

# بیت‌های SYMBOL_FILLING در MQL5؛ در برخی نسخه‌های پکیج MetaTrader5 ثابت SYMBOL_* وجود ندارد
_SYMBOL_FILLING_FOK = 1
_SYMBOL_FILLING_IOC = 2

# قفل سراسری برای تمام تماس‌های MetaTrader5؛ این پکیج thread-safe نیست
# و چند چت می‌توانند به طور همزمان از thread-pool به آن دسترسی پیدا کنند.
# RLock چون برخی توابع توابع دیگر را داخل قفل صدا می‌زنند (ensure_mt5 → reads).
_MT5_LOCK = threading.RLock()


@dataclass
class SplitResult:
    ok: bool
    message: str
    orders_placed: int = 0
    filled_volume: float = 0.0


def volume_digits_from_step(step: float) -> int:
    if step <= 0:
        return 2
    x = float(step)
    digits = 0
    while digits < 8 and abs(x - round(x)) > 1e-12:
        x *= 10.0
        digits += 1
    return digits


def normalize_volume_down(symbol: str, vol: float, clamp_max: bool = True) -> float:
    with _MT5_LOCK:
        info = mt5.symbol_info(symbol)
    if info is None:
        return 0.0
    step = float(info.volume_step or 0.01)
    vmin = float(info.volume_min)
    vmax = float(info.volume_max)
    if step <= 0:
        step = 0.01
    vd = volume_digits_from_step(step)
    v = math.floor(vol / step) * step
    v = round(v, vd)
    if v < vmin - 1e-12:
        return 0.0
    # توجه: volume_max سقف «هر سفارش» است؛ برای حجم کل split نباید به vmax کَپ شود.
    if clamp_max and v > vmax:
        v = vmax
    return v


def _open_volume_for(symbol: str, magic: int) -> float:
    """مجموع حجم پوزیشن‌های باز برای (نماد، magic). برای چک ریسک تجمعی."""
    with _MT5_LOCK:
        positions = mt5.positions_get(symbol=symbol)
    if positions is None:
        return 0.0
    total = 0.0
    for p in positions:
        try:
            if int(getattr(p, "magic", -1)) == int(magic):
                total += float(p.volume)
        except (TypeError, ValueError):
            continue
    return total


def _snapshot_position_volumes(symbol: str, magic: int) -> dict[int, float]:
    """لحظهٔ قبل از شروع split: تیکت → حجم برای (نماد، magic). برای rollback ایمن."""
    with _MT5_LOCK:
        positions = mt5.positions_get(symbol=symbol)
    if not positions:
        return {}
    out: dict[int, float] = {}
    for p in positions:
        try:
            if int(getattr(p, "magic", -1)) != int(magic):
                continue
            out[int(p.ticket)] = float(p.volume)
        except (TypeError, ValueError, AttributeError):
            continue
    return out


def open_volume_for_symbol_magic(
    symbol: str, magic: int, terminal_path: str | None = None
) -> float:
    """API عمومی برای دریافت حجم پوزیشن‌های باز (نماد، magic). 0 اگر MT5 وصل نباشد."""
    ok, _ = ensure_mt5(terminal_path)
    if not ok:
        return 0.0
    return _open_volume_for(symbol, int(magic))


def _side_volumes_from_positions(
    positions: tuple | list | None,
    magic: int | None = None,
) -> dict[str, int | float]:
    """حجم خرید/فروش از لیست پوزیشن؛ اگر magic داده شود فقط همان magic."""
    n_buy = 0
    n_sell = 0
    vol_buy = 0.0
    vol_sell = 0.0
    pos_type_buy = int(getattr(mt5, "POSITION_TYPE_BUY", 0))
    for p in positions or ():
        try:
            if magic is not None and int(getattr(p, "magic", -1)) != int(magic):
                continue
            v = float(p.volume)
            t = int(p.type)
        except (TypeError, ValueError, AttributeError):
            continue
        if t == pos_type_buy:
            n_buy += 1
            vol_buy += v
        else:
            n_sell += 1
            vol_sell += v
    return {
        "n_buy": n_buy,
        "n_sell": n_sell,
        "n_total": n_buy + n_sell,
        "vol_buy": vol_buy,
        "vol_sell": vol_sell,
        "vol_net": vol_buy - vol_sell,
    }


def exposure_summary(
    symbol: str, magic: int, terminal_path: str | None = None
) -> dict:
    """حجم باز خرید/فروش: ربات (magic) در برابر کل پوزیشن‌های همان نماد در MT5."""
    ok, err = ensure_mt5(terminal_path)
    if not ok:
        return {"ok": False, "error": err}

    ok_sym, sym_or_err = _resolve_and_select_symbol(symbol)
    if not ok_sym:
        return {"ok": False, "error": sym_or_err}
    symbol = sym_or_err

    with _MT5_LOCK:
        positions = mt5.positions_get(symbol=symbol)
        all_positions = mt5.positions_get()
        ai = mt5.account_info()

    if positions is None:
        positions = ()
    if all_positions is None:
        all_positions = ()

    bot = _side_volumes_from_positions(positions, int(magic))
    mt5_sym = _side_volumes_from_positions(positions, None)

    profit_pos = 0.0
    profit_neg = 0.0
    acct_net = 0.0
    for p in all_positions:
        try:
            pr = float(getattr(p, "profit", 0.0) or 0.0)
            sw = float(getattr(p, "swap", 0.0) or 0.0)
            cm = float(getattr(p, "commission", 0.0) or 0.0)
            row = pr + sw + cm
        except (TypeError, ValueError, AttributeError):
            continue
        acct_net += row
        if pr >= 0:
            profit_pos += pr
        else:
            profit_neg += pr

    cur = str(getattr(ai, "currency", "USD") or "USD") if ai else "USD"

    return {
        "ok": True,
        "symbol": symbol,
        "magic": int(magic),
        "bot": bot,
        "mt5_symbol": mt5_sym,
        "acct_profit_pos": profit_pos,
        "acct_profit_neg": profit_neg,
        "acct_net": acct_net,
        "currency": cur,
    }


def exposure_panel_html(
    symbol: str, magic: int, terminal_path: str | None = None
) -> str:
    """پنل حجم باز — فقط نمایش لات و سود/ضرر."""
    s = exposure_summary(symbol, magic, terminal_path)
    if not s.get("ok"):
        return f"⚠️  <i>{html.escape(s.get('error', 'خطای MT5'))}</i>"

    sym_h = html.escape(s["symbol"])
    mg = s["magic"]

    def _vol_block(row: dict) -> str:
        vb = float(row["vol_buy"])
        vs = float(row["vol_sell"])
        vn = float(row["vol_net"])
        if vn > 0:
            diff_line = f"🟢  اختلاف بای  ·  <code>{vn:+g}</code> لات"
        elif vn < 0:
            diff_line = f"🔴  اختلاف سل  ·  <code>{vn:+g}</code> لات"
        else:
            diff_line = f"⚪  اختلاف  ·  <code>0</code> لات"
        return (
            f"🟢  بای  ·  <code>{vb:g}</code> لات\n"
            f"🔴  سل  ·  <code>{vs:g}</code> لات\n"
            f"{diff_line}"
        )

    return (
        f"🏷  <code>{sym_h}</code>\n\n"
        f"📊  <b>کل نماد</b>\n"
        f"{_vol_block(s['mt5_symbol'])}\n\n"
        f"🤖  <b>ربات</b>  ·  magic <code>{mg}</code>\n"
        f"{_vol_block(s['bot'])}\n\n"
        f"💹  سود <code>+{float(s['acct_profit_pos']):,.2f}</code>  ·  "
        f"ضرر <code>{float(s['acct_profit_neg']):,.2f}</code>  ·  "
        f"خالص <code>{float(s['acct_net']):+,.2f}</code> {html.escape(s['currency'])}\n\n"
        f"<i>👁  فقط مشاهده — باز یا بسته نمی‌کند.</i>"
    )


def positions_pl_summary(
    symbol: str, magic: int, terminal_path: str | None = None
) -> dict:
    """خلاصه‌ی لحظه‌ای پوزیشن‌های (symbol, magic): تعداد، حجم به تفکیک سمت، سود/ضرر شناور.

    سوآپ و کمیسیون هم جدا شمرده می‌شوند. خالص = profit + swap + commission.
    """
    ok, err = ensure_mt5(terminal_path)
    if not ok:
        return {"ok": False, "error": err}

    ok_sym, sym_or_err = _resolve_and_select_symbol(symbol)
    if not ok_sym:
        return {"ok": False, "error": sym_or_err}
    symbol = sym_or_err

    with _MT5_LOCK:
        positions = mt5.positions_get(symbol=symbol)
        ai = mt5.account_info()

    if positions is None:
        positions = ()

    side = _side_volumes_from_positions(positions, int(magic))
    n_buy = int(side["n_buy"])
    n_sell = int(side["n_sell"])
    vol_buy = float(side["vol_buy"])
    vol_sell = float(side["vol_sell"])
    profit_pos = 0.0
    profit_neg = 0.0
    profit_raw = 0.0
    swap_total = 0.0
    commission_total = 0.0
    n_winners = 0
    n_losers = 0

    pos_type_buy = int(getattr(mt5, "POSITION_TYPE_BUY", 0))
    for p in positions:
        try:
            if int(getattr(p, "magic", -1)) != int(magic):
                continue
            pr = float(getattr(p, "profit", 0.0) or 0.0)
            sw = float(getattr(p, "swap", 0.0) or 0.0)
            cm = float(getattr(p, "commission", 0.0) or 0.0)
            t = int(p.type)
        except (TypeError, ValueError, AttributeError):
            continue

        profit_raw += pr
        swap_total += sw
        commission_total += cm
        # پر کاربر تنها profit را به عنوان «در سود/در ضرر» می‌بیند؛
        # سواپ/کمیسیون جدا گزارش می‌شود.
        if pr >= 0:
            profit_pos += pr
            if pr > 0:
                n_winners += 1
        else:
            profit_neg += pr
            n_losers += 1

    balance = float(getattr(ai, "balance", 0.0) or 0.0) if ai else 0.0
    equity = float(getattr(ai, "equity", 0.0) or 0.0) if ai else 0.0
    margin_used = float(getattr(ai, "margin", 0.0) or 0.0) if ai else 0.0
    margin_free = float(getattr(ai, "margin_free", 0.0) or 0.0) if ai else 0.0
    margin_level = float(getattr(ai, "margin_level", 0.0) or 0.0) if ai else 0.0
    cur = str(getattr(ai, "currency", "USD") or "USD") if ai else "USD"

    return {
        "ok": True,
        "symbol": symbol,
        "magic": int(magic),
        "n_total": n_buy + n_sell,
        "n_buy": n_buy,
        "n_sell": n_sell,
        "n_winners": n_winners,
        "n_losers": n_losers,
        "vol_buy": vol_buy,
        "vol_sell": vol_sell,
        "vol_total": vol_buy + vol_sell,
        "vol_net": vol_buy - vol_sell,
        "profit_pos": profit_pos,
        "profit_neg": profit_neg,
        "profit_raw": profit_raw,
        "swap_total": swap_total,
        "commission_total": commission_total,
        "profit_net": profit_raw + swap_total + commission_total,
        "balance": balance,
        "equity": equity,
        "margin_used": margin_used,
        "margin_free": margin_free,
        "margin_level": margin_level,
        "currency": cur,
    }


def positions_pl_html(
    symbol: str, magic: int, terminal_path: str | None = None
) -> str:
    """گزارش HTML سود/ضرر لحظه‌ای پوزیشن‌های (symbol, magic) برای نمایش در ربات."""
    s = positions_pl_summary(symbol, magic, terminal_path)
    if not s.get("ok"):
        return f"⚠️  <i>{html.escape(s.get('error', 'خطای MT5'))}</i>"

    sym_h = html.escape(s["symbol"])
    cur = html.escape(s["currency"])

    if s["n_total"] == 0:
        return (
            f"🏷  <b>{sym_h}</b>  ·  magic  <code>{s['magic']}</code>\n"
            f"<i>پوزیشن بازی با این Magic روی این نماد نیست.</i>"
        )

    net = s["profit_net"]
    if net > 0:
        net_em = "🟢"
    elif net < 0:
        net_em = "🔴"
    else:
        net_em = "⚪"

    lines: list[str] = [
        f"🏷  <b>{sym_h}</b>  ·  magic  <code>{s['magic']}</code>",
        (
            f"📦  <b>{s['n_total']}</b> پوزیشن  ·  "
            f"🟢 <code>{s['n_buy']}</code>/<code>{s['vol_buy']:g}</code>  ·  "
            f"🔴 <code>{s['n_sell']}</code>/<code>{s['vol_sell']:g}</code>  لات"
        ),
        f"⚖️  خالص حجم  ·  <code>{s['vol_net']:+g}</code>  ·  کل  <code>{s['vol_total']:g}</code>",
        "",
        (
            f"📈  جمع سود  ·  <code>+{s['profit_pos']:,.2f}</code> {cur}  "
            f"<i>({s['n_winners']} پوزیشن)</i>"
        ),
        (
            f"📉  جمع ضرر  ·  <code>{s['profit_neg']:,.2f}</code> {cur}  "
            f"<i>({s['n_losers']} پوزیشن)</i>"
        ),
        f"{net_em}  <b>خالص شناور</b>  ·  <code>{net:+,.2f}</code> {cur}",
    ]

    extras: list[str] = []
    if abs(s["swap_total"]) > 0.005:
        extras.append(f"سواپ <code>{s['swap_total']:+,.2f}</code>")
    if abs(s["commission_total"]) > 0.005:
        extras.append(f"کمیسیون <code>{s['commission_total']:+,.2f}</code>")
    if extras:
        lines.append("·  " + "  ·  ".join(extras))

    if s["equity"] > 0:
        pct_eq = (net / s["equity"]) * 100.0
        lines.append(f"·  نسبت به اکویتی  ·  <code>{pct_eq:+.2f}%</code>")
    if s["margin_level"] > 0:
        ml = s["margin_level"]
        if ml >= 250:
            ml_em = "🟢"
        elif ml >= 150:
            ml_em = "🟡"
        else:
            ml_em = "🔴"
        lines.append(f"📊  سطح مارجین  ·  {ml_em} <code>{ml:,.1f}%</code>")
    if s["balance"] > 0:
        lines.append(
            f"💼  موجودی  <code>{s['balance']:,.2f}</code>  ·  "
            f"اکویتی  <code>{s['equity']:,.2f}</code> {cur}"
        )

    return "\n".join(lines)


def _filling_mode(symbol: str) -> int:
    with _MT5_LOCK:
        info = mt5.symbol_info(symbol)
    if info is None:
        raise RuntimeError(f"نماد نامعتبر: {symbol}")
    fm = int(info.filling_mode)
    order_ioc = int(getattr(mt5, "ORDER_FILLING_IOC", 1))
    order_fok = int(getattr(mt5, "ORDER_FILLING_FOK", 0))
    order_ret = int(getattr(mt5, "ORDER_FILLING_RETURN", 2))
    # اول IOC (پرشدن فوری جزئی) رایج‌تر است
    if fm & _SYMBOL_FILLING_IOC:
        return order_ioc
    if fm & _SYMBOL_FILLING_FOK:
        return order_fok
    return order_ret


def _risk_params(
    override: tuple[float, int, float, float] | None = None,
) -> tuple[float, int, float, float]:
    """سقف حجم، حداکثر سفارش، بافر مارجین ٪، حداقل مارجین آزاد باقیمانده نسبت به اکویتی ٪."""
    if override is not None:
        return override
    raw_mt = os.getenv("RISK_MAX_TOTAL_LOTS", "500").strip()
    max_total = float(raw_mt) if raw_mt else 500.0
    max_ord = int(os.getenv("RISK_MAX_SPLIT_ORDERS", "2000"))
    buf = float(os.getenv("RISK_MARGIN_BUFFER_PERCENT", "10"))
    min_free_eq = float(os.getenv("RISK_MIN_FREE_MARGIN_PERCENT_OF_EQUITY", "5"))
    return max_total, max_ord, buf, min_free_eq


def _safety_absolute_max_execute_lots() -> float | None:
    """سقف سخت هر اجرا از محیط (خاموش اگر خالی یا ≤۰)."""
    raw = os.getenv("SAFETY_ABSOLUTE_MAX_EXECUTE_LOTS", "").strip()
    if not raw:
        return None
    try:
        v = float(raw)
        return v if v > 0 else None
    except ValueError:
        return None


def _safety_expected_mt5_login() -> int | None:
    """اگر در .env ست شود، فقط همان شماره حساب اجازه معامله دارد."""
    for key in ("MT5_EXPECT_LOGIN", "SAFETY_MT5_EXPECTED_LOGIN"):
        raw = os.getenv(key, "").strip()
        if not raw:
            continue
        try:
            return int(raw)
        except ValueError:
            continue
    return None


def _safety_max_spread_points() -> int | None:
    """حداکثر اسپرد نماد (پوینت)؛ اگر نماد گسترده‌تر باشد معامله متوقف می‌شود. خالی=خاموش."""
    raw = os.getenv("SAFETY_MAX_SPREAD_POINTS", "").strip()
    if not raw:
        return None
    try:
        v = int(raw)
        return v if v > 0 else None
    except ValueError:
        return None


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


def _terminal_trade_allowed() -> tuple[bool, str]:
    """قبل از order_send: ترمینال باید معامله الگوریتمی را مجاز کرده باشد."""
    with _MT5_LOCK:
        ti = mt5.terminal_info()
    if ti is None:
        return False, "اطلاعات ترمینال MT5 (terminal_info) در دسترس نیست."
    if not bool(getattr(ti, "trade_allowed", False)):
        return (
            False,
            "معامله الگوریتمی در ترمینال مجاز نیست — در MT5 دکمهٔ «Algo Trading» / AutoTrading را فعال کن.",
        )
    return True, ""


def _account_trade_allowed(ai=None) -> tuple[bool, str]:
    """قبل از order_send: سمت سرور بروکر هم باید برای این اکانت اجازهٔ معامله/EA داده باشد.

    دو فلگ مستقل از تنظیمات کلاینت:
      • ``account_info.trade_allowed`` — کلاً معامله برای این اکانت مجاز است؟
        (False = پسورد Investor / حساب Read-only / محدودیت بروکر)
      • ``account_info.trade_expert``  — معاملهٔ الگوریتمی/EA/API برای این اکانت
        سمت سرور باز است؟ (False = همان حالتی که order_send با
        ``retcode=10026 TRADE_RETCODE_SERVER_DISABLES_AT`` رد می‌شود.)
    """
    if ai is None:
        with _MT5_LOCK:
            ai = mt5.account_info()
    if ai is None:
        return False, "اطلاعات حساب MT5 (account_info) در دسترس نیست."

    if not bool(getattr(ai, "trade_allowed", True)):
        return (
            False,
            (
                "🛑  <b>این اکانت اجازهٔ معامله ندارد</b>\n"
                "<code>account_info.trade_allowed = False</code>\n\n"
                "محتمل‌ترین دلایل:\n"
                "•  با پسورد <b>Investor</b> لاگین شده‌ای (با پسورد Master/Main لاگین کن).\n"
                "•  بروکر حساب را Read-only یا محدود کرده — از ساپورت بخواه «Trade Rights» را فعال کند."
            ),
        )

    if not bool(getattr(ai, "trade_expert", True)):
        login = getattr(ai, "login", "?")
        server = getattr(ai, "server", "?") or "?"
        return (
            False,
            (
                "🛑  <b>سرور بروکر معاملهٔ الگوریتمی این اکانت را بسته است</b>\n"
                f"<code>account_info.trade_expert = False</code>  ·  "
                f"لاگین <code>{html.escape(str(login))}</code> روی "
                f"سرور <code>{html.escape(str(server))}</code>\n\n"
                "این دقیقاً همان چیزی است که هنگام ارسال سفارش به‌صورت "
                "<code>retcode=10026 TRADE_RETCODE_SERVER_DISABLES_AT</code> "
                "«AutoTrading disabled by server» ظاهر می‌شود.\n\n"
                "📩  <b>به ساپورت بروکر بفرست:</b>\n"
                "<blockquote>لطفاً در MT5 Manager روی این اکانت فلگ "
                "«Allow Algo Trading» / «Expert Advisors allowed» را فعال کنید. "
                "این یک تنظیم per-account سمت سرور است و مستقل از تنظیمات کلاینت یا گروه. "
                "از سمت ما <code>terminal_info.trade_allowed=True</code> است؛ "
                "ولی <code>account_info.trade_expert=False</code> یعنی پرچم سرور برای این اکانت خاموش است.</blockquote>"
            ),
        )

    return True, ""


def _count_split_planned_orders(
    symbol: str, total_norm: float, cap_norm: float, step: float
) -> tuple[int | None, str]:
    """همان منطق حلقهٔ split بدون ارسال؛ برای جلوگیری از اجرای ناقص وقتی سقف تعداد کم است."""
    rem = float(total_norm)
    cnt = 0
    cap_loops = 5_000_000
    while rem > step * 0.5 and cnt < cap_loops:
        chunk = min(cap_norm, rem)
        chunk = normalize_volume_down(symbol, chunk)
        if chunk <= 0:
            return (
                None,
                "شبیه‌سازی تقسیم: چانک صفر — استپ حجم با باقیمانده جور نمی‌شود.",
            )
        rem = round(rem - chunk, 10)
        cnt += 1
    if rem > step * 0.5:
        return None, "شبیه‌سازی تقسیم: حجم باقیمانده پس از شبیه‌سازی."
    return cnt, ""


def _send_position_close_deal(
    symbol: str,
    p,
    volume: float,
    deviation: int,
    magic: int,
    close_comment: str,
    filling: int,
) -> tuple[bool, str, float]:
    """یک معاملهٔ بستن مارکت تا حجم vol روی پوزیشن p (از قبل انتخاب‌شده)."""
    ticket = int(p.ticket)
    ptype = int(p.type)
    pos_type_buy = int(getattr(mt5, "POSITION_TYPE_BUY", 0))
    with _MT5_LOCK:
        tick = mt5.symbol_info_tick(symbol)
    if tick is None:
        return False, "tick نیامد", 0.0
    if ptype == pos_type_buy:
        otype = int(getattr(mt5, "ORDER_TYPE_SELL", 1))
        price = float(tick.bid)
    else:
        otype = int(getattr(mt5, "ORDER_TYPE_BUY", 0))
        price = float(tick.ask)
    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": symbol,
        "volume": float(volume),
        "type": otype,
        "position": ticket,
        "price": price,
        "deviation": deviation,
        "magic": int(magic),
        "comment": (close_comment or "")[:31],
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": filling,
    }
    with _MT5_LOCK:
        result = mt5.order_send(request)
        last_err = mt5.last_error() if result is None else None
    if result is None:
        return False, f"order_send None — {last_err}", 0.0
    rcode = int(result.retcode)
    done_codes = {int(mt5.TRADE_RETCODE_DONE), int(getattr(mt5, "TRADE_RETCODE_DONE_PARTIAL", 10010))}
    if rcode not in done_codes:
        return False, f"کد {rcode} — {getattr(result, 'comment', '')}", 0.0
    actual = float(getattr(result, "volume", 0.0) or 0.0)
    if actual <= 0:
        actual = float(volume)
    return True, "", actual


def _rollback_split_session_only(
    symbol: str,
    magic: int,
    deviation: int,
    terminal_path: str | None,
    snapshot_before: dict[int, float],
    filled_total_cap: float,
    close_comment: str = "TgRbck",
) -> SplitResult:
    """فقط حجمی که نسبت به snapshot قبل از split اضافه شده را می‌بندد (حداکثر filled_total_cap لات).

    معاملات دستی قبلی با همان magic روی همان تیکت (Netting) از نظر فنی با حجم جدید
    «مخلوط» می‌شوند؛ این تابع حداکثر به اندازهٔ filled_total_cap از *اضافه‌شدهٔ تخمینی*
    روی هر تیکت بستن را امتحان می‌کند و تیکت‌های کاملاً جدید را کامل می‌بندد.
    """
    ok_init, msg = ensure_mt5(terminal_path)
    if not ok_init:
        return SplitResult(False, msg, 0)

    ok_sym, sym_or_err = _resolve_and_select_symbol(symbol)
    if not ok_sym:
        return SplitResult(False, sym_or_err, 0)
    symbol = sym_or_err

    ok_trade, trade_msg = _terminal_trade_allowed()
    if not ok_trade:
        return SplitResult(False, trade_msg, 0)

    with _MT5_LOCK:
        ai_rb = mt5.account_info()
    if ai_rb is not None:
        ok_l, login_err = _check_account_login(ai_rb)
        if not ok_l:
            return SplitResult(False, login_err, 0)

    filling = _filling_mode(symbol)
    remaining_cap = max(0.0, float(filled_total_cap))
    total_closed = 0.0
    n_deals = 0
    rounds = 0
    last_err = ""

    while remaining_cap > 1e-8 and rounds < 400:
        rounds += 1
        with _MT5_LOCK:
            plist = mt5.positions_get(symbol=symbol)
        if plist is None:
            last_err = "positions_get خطا"
            break
        ours = [p for p in plist if int(getattr(p, "magic", -1)) == int(magic)]
        if not ours:
            break
        progressed = False
        ours.sort(key=lambda q: int(q.ticket))
        for p in ours:
            tid = int(p.ticket)
            try:
                vn = float(p.volume)
            except (TypeError, ValueError):
                continue
            v0 = float(snapshot_before.get(tid, 0.0))
            added = max(0.0, vn - v0)
            if added <= 1e-9:
                continue
            raw_close = min(added, remaining_cap, vn)
            vol = normalize_volume_down(symbol, raw_close)
            if vol <= 0:
                continue
            ok, em, act = _send_position_close_deal(
                symbol, p, vol, deviation, int(magic), close_comment, filling
            )
            if ok and act > 1e-12:
                total_closed = round(total_closed + act, 10)
                remaining_cap = max(0.0, round(remaining_cap - act, 10))
                n_deals += 1
                progressed = True
                break
            last_err = em or "بستن ناموفق"
        if not progressed:
            break

    parts = [
        f"حدود <code>{total_closed:g}</code> لات در <code>{n_deals}</code> معامله بسته شد "
        f"(سقف بازگشت <code>{float(filled_total_cap):g}</code> لات)."
    ]
    if remaining_cap > 1e-6:
        parts.append(
            f"⚠️  حدود <code>{remaining_cap:g}</code> لات از سقف بازگشت باقی ماند "
            f"(مارجین/نماد/دستی). {html.escape(last_err) if last_err else ''}"
        )
    return SplitResult(True, "".join(parts), n_deals, total_closed)


def _maybe_rollback_after_partial_split(
    symbol: str,
    magic: int,
    deviation: int,
    terminal_path: str | None,
    placed: int,
    filled_total: float,
    position_snapshot_before: dict[int, float],
) -> str:
    """اگر AUTO_ROLLBACK_ON_PARTIAL_FAILURE فعال باشد، فقط اضافهٔ نسبت به snapshot را می‌بندد."""
    if placed <= 0 and filled_total <= 1e-12:
        return ""
    if not _env_truthy("AUTO_ROLLBACK_ON_PARTIAL_FAILURE"):
        return ""
    rb = _rollback_split_session_only(
        symbol,
        int(magic),
        deviation,
        terminal_path,
        dict(position_snapshot_before),
        float(filled_total),
    )
    return (
        "\n\n<i>بازگردانی خودکار (فقط حجم اضافه‌شده از ابتدای همین اجرای split، "
        "نسبت به پوزیشن‌های همان magic قبل از اولین سفارش):</i>\n"
        f"{rb.message}"
    )


def _check_account_login(ai) -> tuple[bool, str]:
    exp = _safety_expected_mt5_login()
    if exp is None:
        return True, ""
    got = int(getattr(ai, "login", 0) or 0)
    if got != exp:
        return (
            False,
            f"عدم تطابق حساب: ترمینال روی لاگین {got} است؛ در .env شماره {exp} مجاز تعریف شده.",
        )
    return True, ""


def _symbol_allows_side(info, side: OrderSide) -> bool:
    tm = int(getattr(info, "trade_mode", 4))
    if tm == 0 or tm == 3:
        return False
    if tm == 1 and side == "sell":
        return False
    if tm == 2 and side == "buy":
        return False
    return True


def _margin_check_chunk(
    ai,
    order_type: int,
    symbol: str,
    chunk: float,
    price: float,
    buffer_pct: float,
    min_free_eq_pct: float,
) -> tuple[bool, str]:
    """
    قبل از هر سفارش: مارجین لازم برای همان حجم را با مارجین آزاد مقایسه می‌کند.
    تضمین ضد کال نیست؛ فقط خطای کارگزار/حساب را زودتر و شفاف‌تر می‌کند.
    """
    with _MT5_LOCK:
        req = mt5.order_calc_margin(order_type, symbol, chunk, price)
    if req is None:
        return False, "محاسبه مارجین ناموفق (order_calc_margin). معامله متوقف شد."
    req_f = float(req)
    if req_f <= 0:
        return True, ""
    mult = 1.0 + max(0.0, buffer_pct) / 100.0
    need = req_f * mult
    free = float(ai.margin_free)
    if free < need:
        return (
            False,
            f"مارجین آزاد کافی نیست. نیاز تقریبی این بخش: {req_f:.2f} (+بافر {buffer_pct}%) — آزاد: {free:.2f}",
        )
    if min_free_eq_pct > 0:
        eq = float(getattr(ai, "equity", 0) or 0)
        if eq > 0:
            new_free_est = free - req_f
            floor = eq * (min_free_eq_pct / 100.0)
            if new_free_est < floor:
                return (
                    False,
                    f"بعد از این سفارش مارجین آزاد تخمینی ({new_free_est:.2f}) از "
                    f"{min_free_eq_pct}% اکویتی ({floor:.2f}) کمتر می‌شود — متوقف شد.",
                )
    return True, ""


def _initial_margin_sanity(
    ai,
    order_type: int,
    symbol: str,
    total_lots: float,
    price: float,
    buffer_pct: float,
) -> tuple[bool, str]:
    """یک بار قبل از حلقه: اگر کارگزار مارجین کل را بدهد و آزاد نباشد، اصلاً شروع نکن."""
    with _MT5_LOCK:
        m = mt5.order_calc_margin(order_type, symbol, total_lots, price)
    if m is None:
        return True, ""
    mf = float(m)
    if mf <= 0:
        return True, ""
    mult = 1.0 + max(0.0, buffer_pct) / 100.0
    if float(ai.margin_free) < mf * mult:
        return (
            False,
            f"مارجین آزاد برای کل حجم {total_lots} لات کافی نیست (تخمین کارگزار ~{mf:.2f} + بافر).",
        )
    return True, ""


def ensure_mt5(terminal_path: str | None = None) -> tuple[bool, str]:
    with _MT5_LOCK:
        if mt5.terminal_info() is not None:
            return True, "OK"
        kwargs: dict = {}
        if terminal_path:
            kwargs["path"] = terminal_path
        if not mt5.initialize(**kwargs):
            err = mt5.last_error()
            return False, f"mt5.initialize ناموفق: {err}"
        return True, "OK"


def _resolve_and_select_symbol(symbol: str) -> tuple[bool, str]:
    """نام دقیق نماد در ترمینال متصل را پیدا می‌کند و در مارکت‌واچ انتخاب می‌کند.

    برخی کارگزارها پسوند را با حروف کوچک ثبت می‌کنند (مثلاً ``XAUUSD.p``)؛
    اگر کاربر یا تنظیمات قبلی ``XAUUSD.p`` فرستاده باشد، ``symbol_select`` شکست می‌خورد.
    """
    raw = (symbol or "").strip()
    if not raw:
        return False, "نماد خالی است."
    tried: set[str] = set()

    def _attempt(name: str) -> str | None:
        if not name or name in tried:
            return None
        tried.add(name)
        with _MT5_LOCK:
            if not mt5.symbol_select(name, True):
                return None
            inf = mt5.symbol_info(name)
        if inf is None:
            return None
        canon = getattr(inf, "name", None)
        return str(canon) if canon else name

    for cand in (raw, raw.upper(), raw.lower()):
        got = _attempt(cand)
        if got:
            return True, got

    key = raw.upper()
    matches: list[str] = []
    with _MT5_LOCK:
        all_sym = mt5.symbols_get()
    if all_sym:
        for s in all_sym:
            n = getattr(s, "name", None)
            if not n:
                continue
            ns = str(n)
            if ns.upper() == key:
                matches.append(ns)
    if matches:
        pick = (
            raw
            if raw in matches
            else (raw.upper() if raw.upper() in matches else (raw.lower() if raw.lower() in matches else matches[0]))
        )
        got = _attempt(pick)
        if got:
            return True, got

    with _MT5_LOCK:
        err = mt5.last_error()
    return (
        False,
        (
            f"نماد «{raw}» در بانک نماد ترمینال پیدا نشد یا به مارکت‌واچ اضافه نشد "
            f"(last_error={err!r}). "
            "اگر چند MT5 نصب داری، مسیر ترمینال درست را در MT5_TERMINAL_PATH بگذار؛ "
            "نام را دقیق از Market Watch کپی کن."
        ),
    )


def split_market_orders(
    symbol: str,
    total_lots: float,
    max_lot_per_order: float,
    side: OrderSide,
    deviation: int = 30,
    magic: int = 20260512,
    comment: str = "LotSplitTG",
    max_orders: int | None = None,
    terminal_path: str | None = None,
    risk_tuple: tuple[float, int, float, float] | None = None,
) -> SplitResult:
    ok_init, msg = ensure_mt5(terminal_path)
    if not ok_init:
        return SplitResult(False, msg, 0)

    ok_sym, sym_or_err = _resolve_and_select_symbol(symbol)
    if not ok_sym:
        return SplitResult(False, sym_or_err, 0)
    symbol = sym_or_err

    with _MT5_LOCK:
        info = mt5.symbol_info(symbol)
    if info is None:
        return SplitResult(False, f"symbol_info برای {symbol} نیامد", 0)

    max_sp = _safety_max_spread_points()
    if max_sp is not None:
        sp = int(getattr(info, "spread", 0) or 0)
        if sp > max_sp:
            return SplitResult(
                False,
                f"اسپرد نماد ({sp} پوینت) از سقف امنیتی ({max_sp}) بیشتر است — معامله انجام نشد.",
                0,
            )

    if not _symbol_allows_side(info, side):
        return SplitResult(False, "نماد در این جهت معامله نمی‌شود (حالت نماد / فقط بستن).", 0)

    max_total_cap, max_orders_cap, buf_pct, min_free_eq_pct = _risk_params(risk_tuple)
    abs_cap = _safety_absolute_max_execute_lots()
    if abs_cap is not None:
        if max_total_cap > 0:
            max_total_cap = min(max_total_cap, abs_cap)
        else:
            max_total_cap = abs_cap
    if max_total_cap > 0 and total_lots > max_total_cap:
        return SplitResult(
            False,
            f"حجم کل {total_lots} از سقف امنیتی تنظیم‌شده ({max_total_cap}) بیشتر است.",
            0,
        )

    # چک تجمعی: مجموع پوزیشن‌های باز با همین magic روی همین نماد + درخواست جدید
    # نباید از سقف عبور کند. این جلوی «بیشتر باز شدن از حد در اجراهای متوالی» را می‌گیرد.
    if max_total_cap > 0:
        existing = _open_volume_for(symbol, int(magic))
        if existing + float(total_lots) > max_total_cap + 1e-9:
            return SplitResult(
                False,
                (
                    f"عبور از سقف تجمعی: حجم باز فعلی {existing:g} + درخواست {float(total_lots):g} "
                    f"= {existing + float(total_lots):g} > سقف {max_total_cap:g}."
                ),
                0,
            )

    # سقف تعداد سفارش: اگر صریح پاس نشد، از config می‌آید (بدون سقف سخت 2000)
    if max_orders is None or max_orders <= 0:
        effective_max_orders = max(1, int(max_orders_cap))
    else:
        effective_max_orders = max(1, min(int(max_orders), int(max_orders_cap)))

    sym_max = float(info.volume_max)
    step = float(info.volume_step or 0.01)
    if step <= 0:
        step = 0.01

    cap = min(max_lot_per_order, sym_max)
    cap = normalize_volume_down(symbol, cap)
    if cap <= 0:
        return SplitResult(False, "حداکثر هر سفارش با استپ حجم نماد جور نمی‌شود.", 0)

    if total_lots <= 0:
        return SplitResult(False, "حجم کل باید > 0 باشد.", 0)

    total_req = float(total_lots)
    # حجم کل می‌تواند از volume_max نماد بزرگ‌تر باشد چون بین چند سفارش تقسیم می‌شود.
    total_adj = normalize_volume_down(symbol, total_req, clamp_max=False)
    if total_adj <= 0:
        return SplitResult(
            False,
            "حجم کل با حداقل حجم نماد یا استپ حجم قابل اجرا نیست.",
            0,
        )
    # بدون اجرای «کمتر از درخواست بدون اطلاع»: اگر با کف استپ هم‌راستا نباشد، خطا + نزدیک‌ترین حجم پایین
    if total_req - total_adj > max(step * 0.5, 1e-9):
        return SplitResult(
            False,
            f"حجم کل باید مضرب استپ نماد ({step:g}) باشد. نزدیک‌ترین معتبر پایین: {total_adj:g} لات.",
            0,
        )
    total_lots = float(total_adj)

    planned_n, plan_err = _count_split_planned_orders(symbol, total_lots, cap, step)
    if plan_err:
        return SplitResult(False, plan_err, 0, 0.0)
    if planned_n is not None and planned_n > effective_max_orders:
        return SplitResult(
            False,
            (
                f"این حجم حدود {planned_n} سفارش نیاز دارد؛ سقف مجاز {effective_max_orders} است. "
                f"هیچ سفارشی ارسال نشد."
            ),
            0,
            0.0,
        )

    filling = _filling_mode(symbol)
    order_type = mt5.ORDER_TYPE_BUY if side == "buy" else mt5.ORDER_TYPE_SELL
    remaining = float(total_lots)
    placed = 0
    filled_total = 0.0

    with _MT5_LOCK:
        tick0 = mt5.symbol_info_tick(symbol)
    if tick0 is None:
        return SplitResult(False, "tick نیامد (نماد یا اتصال).", 0)
    price0 = float(tick0.ask if side == "buy" else tick0.bid)

    with _MT5_LOCK:
        ai0 = mt5.account_info()
    if ai0 is None:
        return SplitResult(False, "account_info نیامد.", 0)
    ok_login, login_err = _check_account_login(ai0)
    if not ok_login:
        return SplitResult(False, login_err, 0)
    ok_acc, acc_err = _account_trade_allowed(ai0)
    if not ok_acc:
        return SplitResult(False, acc_err, 0, 0.0)
    ok_pre, pre_err = _initial_margin_sanity(
        ai0, order_type, symbol, float(total_lots), price0, buf_pct
    )
    if not ok_pre:
        return SplitResult(False, pre_err, 0)

    ok_trade, trade_msg = _terminal_trade_allowed()
    if not ok_trade:
        return SplitResult(False, trade_msg, 0, 0.0)

    pos_snap = _snapshot_position_volumes(symbol, int(magic))

    done_codes = {int(mt5.TRADE_RETCODE_DONE)}
    partial_code = int(getattr(mt5, "TRADE_RETCODE_DONE_PARTIAL", 10010))
    done_codes.add(partial_code)

    def _rb() -> str:
        return _maybe_rollback_after_partial_split(
            symbol,
            int(magic),
            deviation,
            terminal_path,
            placed,
            filled_total,
            pos_snap,
        )

    while remaining > step * 0.5 and placed < effective_max_orders:
        chunk = min(cap, remaining)
        chunk = normalize_volume_down(symbol, chunk)
        if chunk <= 0:
            return SplitResult(
                False,
                "باقیمانده با استپ حجم کارگزار جور نمی‌شود." + _rb(),
                placed,
                filled_total,
            )

        with _MT5_LOCK:
            tick = mt5.symbol_info_tick(symbol)
        if tick is None:
            return SplitResult(False, "tick نیامد (نماد یا اتصال)." + _rb(), placed, filled_total)

        price = tick.ask if side == "buy" else tick.bid
        with _MT5_LOCK:
            ai = mt5.account_info()
        if ai is None:
            return SplitResult(False, "account_info قطع شد." + _rb(), placed, filled_total)
        ok_m, m_err = _margin_check_chunk(
            ai, order_type, symbol, chunk, float(price), buf_pct, min_free_eq_pct
        )
        if not ok_m:
            return SplitResult(
                False,
                m_err + f" — تا اینجا {placed} سفارش / {filled_total:g} لات." + _rb(),
                placed,
                filled_total,
            )
        request = {
            "action": mt5.TRADE_ACTION_DEAL,
            "symbol": symbol,
            "volume": chunk,
            "type": order_type,
            "price": price,
            "deviation": deviation,
            "magic": int(magic),
            "comment": comment[:31] if comment else "",
            "type_time": mt5.ORDER_TIME_GTC,
            "type_filling": filling,
        }

        with _MT5_LOCK:
            result = mt5.order_send(request)
            last_err = mt5.last_error() if result is None else None
        if result is None:
            return SplitResult(
                False,
                f"order_send None — {last_err} — تا اینجا {filled_total:g} لات." + _rb(),
                placed,
                filled_total,
            )

        rcode = int(result.retcode)
        # حجم واقعاً پر شده‌ی این سفارش: ترجیحاً از خود result.volume (پُرشدن جزئی).
        actual = float(getattr(result, "volume", 0.0) or 0.0)
        if rcode not in done_codes:
            return SplitResult(
                False,
                (
                    f"رد کد: {rcode} — {result.comment} — "
                    f"تا اینجا {placed} سفارش / {filled_total:g} لات."
                )
                + _rb(),
                placed,
                filled_total,
            )

        # اگر کارگزار حجم را برنگرداند، فرض می‌کنیم همان chunk کامل پر شده (retcode = DONE).
        if actual <= 0:
            actual = chunk

        filled_total = round(filled_total + actual, 10)
        remaining = round(remaining - actual, 10)
        placed += 1

        # اگر سفارش جزئی پر شد (actual < chunk)، یعنی کارگزار همه را نگرفت؛
        # تلاش برای ادامه اوکی است ولی برای جلوگیری از حلقه‌ی نامحدود با چانک خیلی کوچک‌تر،
        # بررسی می‌کنیم که اگر باقی‌مانده هنوز قابل اجرا نیست متوقف شویم.
        if actual + 1e-12 < chunk:
            # ادامه‌ی حلقه: همان منطق normalize_volume_down چانک بعدی را تنظیم می‌کند.
            pass

    if remaining > step * 0.5:
        return SplitResult(
            False,
            (
                f"توقف: سقف تعداد سفارش‌ها رسید یا باقیمانده نامعتبر. "
                f"پُرشده: {filled_total:g} لات از {float(total_lots):g}."
            )
            + _rb(),
            placed,
            filled_total,
        )

    side_fa = "خرید" if side == "buy" else "فروش"
    side_em = "🟢" if side == "buy" else "🔴"
    return SplitResult(
        True,
        (
            f"{side_em}  <b>{side_fa} انجام شد</b>\n"
            f"🏷  {symbol}  ·  📦  <code>{filled_total:g}</code> لات  ·  "
            f"⚡  <code>{placed}</code> سفارش"
        ),
        placed,
        filled_total,
    )


# بستن پوزیشن از تلگرام حذف شد — ربات فقط باز کردن و مشاهده دارد.
# rollback ایمن split هنوز از _send_position_close_deal داخلی استفاده می‌کند.


def mt5_connection_status() -> str:
    with _MT5_LOCK:
        ti = mt5.terminal_info()
        if ti is None:
            return "MT5 وصل نیست (ترمینال را باز کن؛ اسکریپت باید mt5.initialize بزند)."
        ai = mt5.account_info()
    acc = ""
    if ai:
        acc = f"حساب: {ai.login} | سرور: {ai.server} | تعادل: {ai.balance}"
    trade_ok = bool(getattr(ti, "trade_allowed", False))
    return f"MT5 OK | معامله مجاز ترمینال: {trade_ok}\n{acc}"


def account_mini_line_html(terminal_path: str | None = None) -> str:
    """یک خط فشرده برای مرحلهٔ قبل از تأیید."""
    ok, err = ensure_mt5(terminal_path)
    if not ok:
        return f"⚠️  <i>{html.escape(err)}</i>"
    with _MT5_LOCK:
        ai = mt5.account_info()
    if ai is None:
        return "⚠️  <i>اطلاعات حساب در دسترس نیست.</i>"
    bal = float(ai.balance)
    eq = float(ai.equity)
    free = float(ai.margin_free)
    cur = html.escape(str(getattr(ai, "currency", "USD")))
    return (
        f"💼  موجودی  ·  <code>{bal:,.2f}</code> {cur}\n"
        f"📈  اکویتی  ·  <code>{eq:,.2f}</code> {cur}\n"
        f"💨  مارجین آزاد  ·  <code>{free:,.2f}</code> {cur}"
    )


def account_confirm_block_html(
    symbol: str,
    side: OrderSide,
    total_lots: float,
    terminal_path: str | None = None,
) -> str:
    """
    بلوک HTML برای مرحلهٔ تأیید: حساب فعلی + تخمین مارجین برای کل حجم.
    همهٔ اعداد تقریبی‌اند (قیمت لحظه‌ای، بدون سوآپ آینده).
    """
    ok, err = ensure_mt5(terminal_path)
    if not ok:
        return f"⚠️  <i>{html.escape(err)}</i>"

    with _MT5_LOCK:
        ai = mt5.account_info()
    if ai is None:
        return "⚠️  <i>اطلاعات حساب در دسترس نیست.</i>"

    ok_sym, sym_or_err = _resolve_and_select_symbol(symbol)
    if not ok_sym:
        return f"⚠️  <i>{html.escape(sym_or_err)}</i>"
    symbol = sym_or_err

    cur = html.escape(str(getattr(ai, "currency", "USD")))
    login = int(getattr(ai, "login", 0) or 0)
    server = html.escape(str(getattr(ai, "server", "") or ""))
    lev = int(getattr(ai, "leverage", 0) or 0)

    balance = float(ai.balance)
    equity = float(ai.equity)
    profit = float(getattr(ai, "profit", equity - balance) or 0.0)
    margin_used = float(ai.margin)
    margin_free = float(ai.margin_free)
    ml_now = float(getattr(ai, "margin_level", 0.0) or 0.0)

    lines: list[str] = []

    acc_trade_ok = bool(getattr(ai, "trade_allowed", True))
    acc_expert_ok = bool(getattr(ai, "trade_expert", True))
    if not (acc_trade_ok and acc_expert_ok):
        if not acc_trade_ok:
            warn = "این اکانت اجازهٔ معامله ندارد (پسورد Investor یا Read-only)."
        else:
            warn = (
                "بروکر فلگ Algo/EA این اکانت را سمت سرور بسته نگه داشته "
                "— اجرا با <code>retcode 10026</code> رد می‌شود."
            )
        lines += [
            f"🛑  <b>هشدار پیش از اجرا</b>  ·  {warn}",
            "",
        ]

    lines += [
        f"🏦  <b>حساب</b>  ·  <code>{login}</code>  ·  {server}",
        f"⚖️  اهرم  <code>1:{lev}</code>  ·  واحد  <b>{cur}</b>",
        "",
        f"💼  موجودی  ·  <code>{balance:,.2f}</code>",
        f"📈  اکویتی  ·  <code>{equity:,.2f}</code>  ·  شناور  <code>{profit:+,.2f}</code>",
        f"💰  مارجین مصرف‌شده  ·  <code>{margin_used:,.2f}</code>",
        f"💨  مارجین آزاد  ·  <code>{margin_free:,.2f}</code>",
    ]
    if ml_now > 0:
        if ml_now >= 250:
            ml_em = "🟢"
        elif ml_now >= 150:
            ml_em = "🟡"
        else:
            ml_em = "🔴"
        lines.append(f"📊  سطح مارجین  ·  {ml_em} <code>{ml_now:,.1f}%</code>")

    order_type = mt5.ORDER_TYPE_BUY if side == "buy" else mt5.ORDER_TYPE_SELL
    m_est: float | None = None
    price = 0.0
    with _MT5_LOCK:
        tick = mt5.symbol_info_tick(symbol)
        if tick is not None:
            price = float(tick.ask if side == "buy" else tick.bid)
            raw = mt5.order_calc_margin(order_type, symbol, float(total_lots), price)
            if raw is not None:
                m_est = float(raw)

    side_fa = "خرید" if side == "buy" else "فروش"
    side_em = "🟢" if side == "buy" else "🔴"
    lines += ["", f"📐  <b>تخمین این سفارش</b>  <i>· لحظه‌ای</i>"]
    if tick is None:
        lines.append("·  قیمت نماد نیامد — مارجین قابل برآورد نیست.")
    else:
        lines.append(
            f"{side_em}  {side_fa}  ·  قیمت  <code>{price:,.5f}</code>  ·  حجم  <code>{total_lots:g}</code> لات"
        )
        if m_est is not None and m_est > 0:
            new_used = margin_used + m_est
            est_free_after = margin_free - m_est
            lines.append(f"💰  مارجین لازم  ·  <code>{m_est:,.2f}</code> {cur}")
            lines.append(f"💨  آزاد بعد از همه  ·  <code>{est_free_after:,.2f}</code>")
            if new_used > 1e-9 and equity > 0:
                proj_ml = (equity / new_used) * 100.0
                if proj_ml >= 250:
                    proj_em, proj_note = "🟢", ""
                elif proj_ml >= 150:
                    proj_em, proj_note = "🟡", "  <i>· احتیاط</i>"
                else:
                    proj_em, proj_note = "🔴", "  <b>· خطر — نزدیک استاپ‌اوت</b>"
                lines.append(
                    f"📊  سطح مارجین بعد از همه  ·  {proj_em} <code>{proj_ml:,.1f}%</code>{proj_note}"
                )
            if est_free_after < 0:
                lines.append(
                    "🛑  <b>احتمال رد سفارش</b>  ·  <i>مارجین آزاد فعلی از تخمین نیاز کمتر است.</i>"
                )
        else:
            lines.append("·  مارجین این حجم از API نیامد — قبل از اجرا در MT5 چک کن.")

    lines.append("")
    lines.append(
        "<i>با حرکت قیمت، سوآپ، اسپرد و هج اعداد تغییر می‌کنند؛ این فقط برآورد لحظه است.</i>"
    )
    return "\n".join(lines)


def mt5_connection_status_html(terminal_path: str | None = None) -> str:
    """وضعیت اتصال + خلاصهٔ حساب برای نمایش HTML در ربات (خط‌به‌خط)."""
    ok, err = ensure_mt5(terminal_path)
    if not ok:
        return f"❌  {html.escape(err)}"
    with _MT5_LOCK:
        ti = mt5.terminal_info()
        ai = mt5.account_info()
    trade_ok = bool(getattr(ti, "trade_allowed", False)) if ti else False
    state_em = "🟢" if trade_ok else "🔴"
    state_lbl = "مجاز" if trade_ok else "غیرمجاز"
    acc_trade_ok = bool(getattr(ai, "trade_allowed", False)) if ai else False
    acc_expert_ok = bool(getattr(ai, "trade_expert", False)) if ai else False
    acc_em = "🟢" if (acc_trade_ok and acc_expert_ok) else "🔴"
    if acc_trade_ok and acc_expert_ok:
        acc_lbl = "مجاز"
    elif not acc_trade_ok:
        acc_lbl = "غیرمجاز (Investor/Read-only؟)"
    else:
        acc_lbl = "غیرمجاز  ·  retcode 10026 از سرور"
    lines: list[str] = [
        f"🔌  <b>وضعیت اتصال</b>  ·  متصل ✓",
        f"⚡  <b>معامله الگوریتمی — ترمینال</b>  ·  {state_em} {state_lbl}",
        f"🛰  <b>معامله الگوریتمی — سرور بروکر</b>  ·  {acc_em} {acc_lbl}",
    ]
    if ai and not (acc_trade_ok and acc_expert_ok):
        login = int(getattr(ai, "login", 0) or 0)
        server = html.escape(str(getattr(ai, "server", "") or ""))
        lines += [
            "",
            "🛑  <b>چه شد:</b> بروکر این اکانت را برای معاملهٔ الگوریتمی/API بسته نگه داشته.",
            f"📩  <i>به ساپورت بگو: «روی لاگین </i><code>{login}</code><i> سرور </i>"
            f"<code>{server}</code><i> فلگ <b>Allow Algo Trading</b> (Expert Advisors allowed) "
            "را در MT5 Manager فعال کنید.»</i>",
        ]
    if ai:
        cur = html.escape(str(getattr(ai, "currency", "USD")))
        bal = float(ai.balance)
        eq = float(ai.equity)
        free = float(ai.margin_free)
        login = int(getattr(ai, "login", 0) or 0)
        server = html.escape(str(getattr(ai, "server", "") or ""))
        lev = int(getattr(ai, "leverage", 0) or 0)
        lines += [
            "",
            f"👤  <b>حساب</b>  ·  <code>{login}</code>  ·  {server}",
            f"⚖️  <b>اهرم</b>  ·  <code>1:{lev}</code>  ·  واحد  <b>{cur}</b>",
            "",
            f"💼  موجودی  ·  <code>{bal:,.2f}</code> {cur}",
            f"📈  اکویتی  ·  <code>{eq:,.2f}</code> {cur}",
            f"💨  مارجین آزاد  ·  <code>{free:,.2f}</code> {cur}",
        ]
        ml = float(getattr(ai, "margin_level", 0) or 0)
        if ml > 0:
            if ml >= 250:
                em = "🟢"
            elif ml >= 150:
                em = "🟡"
            else:
                em = "🔴"
            lines.append(f"📊  سطح مارجین  ·  {em} <code>{ml:,.1f}%</code>")
    else:
        lines.append("")
        lines.append("<i>اطلاعات حساب در دسترس نیست.</i>")
    return "\n".join(lines)
