"""env 配置页 UI 白名单:中文标签,按交易所过滤."""
from __future__ import annotations

import os
from typing import Any, Optional

from lib.env.env_file_lib import env_get_all, read_env_lines
from lib.env.env_schema import (
    _field_type,
    _hot_reload,
    _is_sensitive,
    _mask_value,
    _restart_required,
    normalize_select_value,
    parse_env_example_schema,
    select_options_for,
)

# 各所「交易所与实盘」字段(顺序即页面顺序)
_EXCHANGE_LIVE_FIELDS: dict[str, list[tuple[str, str, str]]] = {
    "okx": [
        ("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"),
        ("OKX_API_KEY", "API Key", "账户 API(永续+期权共用)"),
        ("OKX_API_SECRET", "API Secret", "账户 API(永续+期权共用)"),
        ("OKX_API_PASSPHRASE", "API Passphrase", "OKX 必填"),
        ("OKX_TD_MODE", "保证金模式", ""),
        ("OKX_POS_MODE", "持仓模式", ""),
        ("OKX_POSITION_INST_TYPE", "仓位查询类型", "如 SWAP"),
        ("OKX_ACCOUNT_LABEL", "账户备注", "企业微信推送中显示"),
        (
            "OKX_SHOW_PERP_FUNDS",
            "显示永续资金",
            "默认开启;关闭后顶栏隐藏 USDT 资金账户与交易账户,总资金仅计期权 USDC 侧",
        ),
    ],
    "binance": [
        ("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"),
        ("BINANCE_API_KEY", "API Key", "永续子账户"),
        ("BINANCE_API_SECRET", "API Secret", "永续子账户"),
        ("BINANCE_MARGIN_MODE", "保证金模式", ""),
        ("BINANCE_POSITION_MODE", "持仓模式", ""),
        ("BINANCE_ACCOUNT_LABEL", "账户备注", "企业微信推送中显示"),
    ],
    "gate": [
        ("LIVE_TRADING_ENABLED", "开启实盘下单", "关闭时仅走本地流程,不向交易所发单"),
        ("GATE_API_KEY", "API Key", "永续子账户"),
        ("GATE_API_SECRET", "API Secret", "永续子账户"),
        ("GATE_TD_MODE", "保证金模式", ""),
        ("GATE_POS_MODE", "持仓模式", ""),
        ("GATE_ACCOUNT_LABEL", "账户备注", "企业微信推送中显示"),
    ],
}

_SHARED_SECTIONS: list[dict[str, Any]] = [
    {
        "title": "企业微信",
        "fields": [
            ("WECHAT_WEBHOOK", "机器人 Webhook", "行情与风控推送地址"),
            ("WECHAT_TIMEOUT_SECONDS", "推送超时(秒)", "默认 10"),
        ],
    },
    {
        "title": "交易执行",
        "fields": [
            ("POSITION_SIZING_MODE", "计仓模式", "切换须无仓后重启"),
            ("RISK_PERCENT", "以损定仓风险%", "单笔风险占资金比例"),
            ("FULL_MARGIN_BUFFER_RATIO", "全仓资金缓冲比例", "如 0.98"),
            ("BTC_LEVERAGE", "BTC 默认杠杆", ""),
            ("ALT_LEVERAGE", "山寨默认杠杆", ""),
            ("TRADE_DIRECTION_RESTRICT_ENABLED", "方向限制开关", ""),
            ("TRADE_DIRECTION", "允许方向", "需同时开启「方向限制开关」才生效"),
            ("TRADE_SYMBOL_RESTRICT_ENABLED", "币种白名单开关", ""),
            ("TRADE_SYMBOL_WHITELIST", "白名单币种", "逗号分隔,如 BTC,ETH"),
            ("TRADING_DAY_RESET_HOUR", "交易日切点(北京时间)", "整点,默认 8"),
            (
                "TRADING_DAY_RESET_OPEN_GUARD_ENABLED",
                "切点前禁止新开仓",
                "默认 true;开启则北京时间切点前禁止斐波登记与人工开仓;说明见风控说明·交易执行",
            ),
            ("MAX_ACTIVE_POSITIONS", "最大同时持仓", ""),
            ("MANUAL_MIN_PLANNED_RR", "人工最低盈亏比", "如 1.4"),
            ("KEY_AUTO_ORDER_ENABLED", "关键位自动单", "关闭后箱体/收敛/斐波等不自动开仓;支撑阻力提醒仍可用"),
            ("KEY_AUTO_MIN_PLANNED_RR", "关键位最低盈亏比", "自动单计划 RR 须严格大于该值,默认 1.5"),
            ("FORCE_CLOSE_ENABLED", "强制清仓开关", ""),
            ("FORCE_CLOSE_BJ_HOUR", "强制清仓整点(北京)", ""),
            ("FORCE_CLOSE_GRACE_MINUTES", "强制清仓窗口(分钟)", "默认 5;整点起该分钟内执行并禁止开仓"),
        ],
    },
    {
        "title": "交易风控",
        "fields": [
            ("DAILY_OPEN_ALERT_THRESHOLD", "单日开仓提醒阈值", "达次数后 AI 提醒,不拦单"),
            ("DAILY_OPEN_HARD_LIMIT", "单日开仓硬上限", "0=不启用"),
        ],
    },
    {
        "title": "账户冷静期",
        "fields": [
            ("RISK_CONTROL_ENABLED", "冷静期总开关", ""),
            ("RISK_COOLING_HOURS_MANUAL", "手动平仓冷静(小时)", ""),
            ("RISK_COOLING_HOURS_MANUAL_JOURNAL", "复盘情绪冷静(小时)", ""),
            ("RISK_MANUAL_CLOSE_DAILY_LIMIT", "日手动平仓次数上限", ""),
            ("RISK_DAILY_LOSS_LIMIT", "日亏损次数上限", "默认2;达限当日冻结开仓;0=不因亏损次数冻结"),
            ("RISK_MOOD_ISSUES_DAILY_FREEZE", "情绪标签日冻结", ""),
        ],
    },
    {
        "title": "自动划转",
        "fields": [
            ("AUTO_TRANSFER_ENABLED", "启用自动划转", ""),
            ("AUTO_TRANSFER_AMOUNT", "目标余额(U)", "交易账户目标 USDT"),
            ("AUTO_TRANSFER_FROM", "划出账户", "余额不足时从此账户划入交易账户"),
            ("AUTO_TRANSFER_TO", "划入账户", "目标余额所在账户,一般为 swap"),
            ("AUTO_TRANSFER_BJ_HOUR", "执行整点(北京时间)", ""),
            ("TRANSFER_CCY", "划转币种", ""),
        ],
    },
    {
        "title": "当日资金",
        "fields": [
            ("DAILY_START_CAPITAL", "日起始基数(U)", ""),
            ("DAILY_LOSS_CAPITAL", "回撤后基数(U)", ""),
            ("DAILY_PROFIT_CAPITAL", "盈利后基数(U)", ""),
        ],
    },
]

_MODE_SECTION: dict[str, Any] = {
    "title": "期权/对冲模式",
    "exchanges": frozenset({"okx"}),
    "fields": [
        (
            "OKX_TRADE_MODE",
            "交易模式",
            "三选一:单独期权 / 永期对冲 / 期期对冲.选单独期权时隐藏对冲导航与对冲配置;选对冲时不可单独开期权",
        ),
    ],
}

_OPTIONS_SECTION: dict[str, Any] = {
    "title": "期权账户",
    "exchanges": frozenset({"okx"}),
    "fields": [
        ("OKX_OPTIONS_ENABLED", "启用期权模块", "与永续共用上方 OKX_API_*;不再单独配置期权密钥"),
        ("OKX_OPTIONS_ACCOUNT_LABEL", "期权账户备注", ""),
        (
            "OKX_OPTIONS_MARGIN_MODE",
            "单笔期权本位",
            "usdc=USDⓈ权利金;coin=币本位+USDT买币桥(默认)。有持仓/半成品桥时勿切换;改后需重启",
        ),
        (
            "OKX_OPTIONS_TRADE_BUDGET_USDC",
            "单笔预算(USDC)",
            "仅 USDC 模式且全仓复利关闭时显示/生效;用于「按可用余额打满」及张数/币数上限",
        ),
        ("OKX_OPTIONS_BUDGET_BUFFER", "预算缓冲比例", "如 0.95;USDC 打满/全仓复利与币本位复利共用"),
        (
            "OKX_OPTIONS_COIN_COMPOUND",
            "币本位按交易户USDT复利",
            "默认 true;预算=交易账户USDT×缓冲;关闭则用下方固定 USDT 预算×缓冲",
        ),
        (
            "OKX_OPTIONS_COIN_BUDGET_USDT",
            "币本位固定预算(USDT)",
            "仅币本位且复利关闭时生效",
        ),
        (
            "OKX_OPTIONS_COIN_MAX_USDT_ENABLED",
            "币本位单笔上限开关",
            "默认 false=靠人工转走控规模;true 时预算不超过下方 N U",
        ),
        (
            "OKX_OPTIONS_COIN_MAX_USDT",
            "币本位单笔上限(USDT)",
            "仅上限开关开启时生效",
        ),
        (
            "OKX_OPTIONS_COIN_SPOT_BUY_BUFFER",
            "币本位现货买入缓冲",
            "相对权利金倍数,默认 1.10(=多买10%);也可写 0.10 表示+10%。按最大可开张数×卖一权利金×本缓冲买币,不全额兑换",
        ),
        (
            "OKX_OPTIONS_COMPOUND_FULL_ENABLED",
            "全仓复利开关",
            "默认 true;仅 USDC 模式。开启时隐藏单笔预算且不可用打满预算,下单以全仓复利为主;关闭则恢复单笔预算并隐藏全仓复利",
        ),
        (
            "OKX_OPTIONS_COMPOUND_FULL_CAP_ENABLED",
            "全仓复利上限开关",
            "仅全仓复利开启时有意义;默认 false=不设上限用期权户全部可用;true 时按下方上限封顶",
        ),
        (
            "OKX_OPTIONS_COMPOUND_FULL_CAP_USDC",
            "全仓复利上限(USDC)",
            "仅「全仓复利」且「上限开关」都开启时生效;例如 300",
        ),
        (
            "OKX_OPTIONS_MAX_ACTIVE_POSITIONS",
            "期权持仓上限(笔)",
            "仅「单独期权」模式生效;默认 0=不限制;按交易所期权合约笔数计数,同合约加仓不占新笔数",
        ),
        ("OKX_OPTIONS_DEFAULT_UNDERLY", "默认标的", "如 ETH"),
        (
            "OKX_OPTIONS_CHAIN_MAX_DTE_DAYS",
            "期权链展示天数",
            "默认 14;下拉到期日只出现该天数内的合约(含明天)",
        ),
        (
            "OKX_OPTIONS_MAX_DTE_DAYS",
            "开仓最大剩余天数",
            "默认 2;单独开期权时拒绝更远到期(与链展示天数独立)",
        ),
        (
            "OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED",
            "链上仅显示有卖一",
            "默认 true;开启后隐藏无卖一深度或深度不足1张的合约(含标记价估算行)",
        ),
        (
            "OKX_OPTIONS_CLOSE_GATE_MODE",
            "目标平仓门控模式",
            "premium=可回收(U)≥权利金(U)×倍数;net_pnl=净盈亏(U)大于阈值。币本位按指数换算为U",
        ),
        (
            "OKX_OPTIONS_CLOSE_RECYCLE_MULT",
            "门控权利金倍数(全局)",
            "可选;填写则覆盖下方分本位默认值",
        ),
        (
            "OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN",
            "门控权利金倍数(币本位)",
            "默认 1.05;premium 模式下 recyclable(U)≥premium(U)×本值",
        ),
        (
            "OKX_OPTIONS_CLOSE_RECYCLE_MULT_USDC",
            "门控权利金倍数(USDC)",
            "默认 2;premium 模式下 recyclable(U)≥premium(U)×本值",
        ),
        (
            "OKX_OPTIONS_CLOSE_NET_PNL_MIN_U",
            "门控净盈亏下限(U)",
            "net_pnl 模式;净盈亏(估)须大于本值,如 0 或 1",
        ),
        (
            "OKX_OPTIONS_CLOSE_HOLD_SECONDS",
            "门控持续秒数",
            "达标后须持续本秒数才通过,默认 120",
        ),
    ],
}

# 对冲公共字段(不含已由 OKX_TRADE_MODE 取代的 ENABLED/SHOW/MUTUAL)
_HEDGE_COMMON_FIELDS: list[tuple[str, str, str]] = [
    ("HEDGE_PLAN_LIVE_ORDER", "允许对冲真实下单", "再与实盘 LIVE_TRADING_ENABLED 同开才可启动"),
    (
        "MAX_ACTIVE_HEDGE_PLANS",
        "对冲组数上限",
        "默认 1;同时进行中的对冲计划组数(opening/active/partial),可改",
    ),
    ("HEDGE_PLAN_MONITOR_POLL_SECONDS", "对冲监控轮询(秒)", "默认 15"),
    (
        "HEDGE_PLAN_BUDGET_BUFFER",
        "对冲预算缓冲比例",
        "默认 0.95;仅对冲计划;与期权页 OKX_OPTIONS_BUDGET_BUFFER 独立",
    ),
    (
        "HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL",
        "半腿失败改手动补开",
        "默认 true;开启时半腿失败不自动平,计划挂 partial,页面可补开;并强制关闭下方自动平",
    ),
    (
        "HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION",
        "半腿失败时自动平期权",
        "默认 true;若上方「半腿失败改手动补开」开启则本项强制无效",
    ),
]

_HEDGE_PO_FIELDS: list[tuple[str, str, str]] = [
    (
        "HEDGE_PLAN_OPTION_PRIMARY",
        "永期模式(以期权为主/保险)",
        "默认 true=以期权为主;false=保险模式;页面标题前显示标识,不可在页内切换",
    ),
    ("HEDGE_PLAN_OPEN_ORDER", "永期开仓顺序", "options_first 或 perp_first"),
    ("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", "永期止损后强制平期权", "保护机制,建议保持 true"),
    ("HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", "永期止盈后强制平期权", "默认 false,保险腿不平"),
    (
        "HEDGE_PLAN_ITM_MAX_DIST_USD",
        "永期实值最大深度(U)",
        "默认空=沿用 OKX_OPTIONS_ITM_MAX_DIST_USD(常 30);0=不限制",
    ),
    (
        "HEDGE_PLAN_MIN_OPTION_HOURS",
        "对冲期权最低剩余小时",
        "默认 8;测算/启动时若传 hours_to_expiry 则校验",
    ),
    (
        "HEDGE_PLAN_MIN_OPTION_LEVERAGE",
        "对冲期权最低杠杆(S/ask)",
        "默认 0=不启用;>0 时拒绝杠杆过低的保险腿",
    ),
]

_HEDGE_OO_FIELDS: list[tuple[str, str, str]] = [
    ("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", "期期只平盈利腿", "达目标价只平盈利方"),
    (
        "HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
        "期期平仓模式(方案C)",
        "默认 true;开启后页面可选「到期平/全平」;关闭则固定到期平",
    ),
    (
        "HEDGE_PLAN_OO_BIAS_SPLIT_BY",
        "期期做多做空拆分口径",
        "默认预算金额;budget=按权利金预算分两腿;sheets=先算同张数再按比例拆",
    ),
    (
        "HEDGE_PLAN_OO_BIAS_RATIO",
        "期期做多做空主腿占比",
        "默认 0.7(即 7:3);做多主腿=Call,做空主腿=Put;须在 0~1 之间",
    ),
]

# 兼容旧测试/全量字段列表(写 env 时仍允许这些键,但 UI 按模式过滤)
_HEDGE_PLAN_SECTION: dict[str, Any] = {
    "title": "对冲计划",
    "exchanges": frozenset({"okx"}),
    "fields": [
        ("HEDGE_PLAN_ENABLED", "启用对冲计划", "已由「交易模式」取代,一般无需再改"),
        ("HEDGE_PLAN_SHOW_PERP_OPTIONS", "显示永期对冲", "已由「交易模式」取代"),
        ("HEDGE_PLAN_SHOW_OPTIONS_OPTIONS", "显示期期对冲", "已由「交易模式」取代"),
        ("HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE", "对冲与期权互斥门控", "已由「交易模式」三选一取代"),
        *_HEDGE_COMMON_FIELDS,
        *_HEDGE_PO_FIELDS,
        *_HEDGE_OO_FIELDS,
    ],
}


# 与运行时 os.getenv 默认一致;.env 未写明时展示实际生效值(同风控说明页)
_RUNTIME_ENV_DEFAULTS: dict[str, str] = {
    "RISK_CONTROL_ENABLED": "true",
    "RISK_COOLING_HOURS_MANUAL": "4",
    "RISK_COOLING_HOURS_MANUAL_JOURNAL": "1",
    "RISK_MANUAL_CLOSE_DAILY_LIMIT": "2",
    "RISK_DAILY_LOSS_LIMIT": "2",
    "RISK_MOOD_ISSUES_DAILY_FREEZE": "true",
    "AUTO_TRANSFER_FROM": "funding",
    "AUTO_TRANSFER_TO": "swap",
    "TRANSFER_CCY": "USDT",
    "HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
    "HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
    "OKX_SHOW_PERP_FUNDS": "true",
    "OKX_OPTIONS_CHAIN_ASK_LIQ_FILTER_ENABLED": "true",
    "OKX_OPTIONS_CHAIN_MAX_DTE_DAYS": "14",
    "OKX_OPTIONS_MAX_DTE_DAYS": "2",
    "OKX_OPTIONS_MAX_ACTIVE_POSITIONS": "0",
    "OKX_TRADE_MODE": "options",
    "MAX_ACTIVE_HEDGE_PLANS": "1",
    "HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true",
    "HEDGE_PLAN_OO_BIAS_SPLIT_BY": "budget",
    "HEDGE_PLAN_OO_BIAS_RATIO": "0.7",
    "HEDGE_PLAN_BUDGET_BUFFER": "0.95",
    "HEDGE_PLAN_OPTION_PRIMARY": "true",
    "HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true",
    "HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL": "true",
    "OKX_OPTIONS_CLOSE_GATE_MODE": "premium",
    "OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN": "1.05",
    "OKX_OPTIONS_CLOSE_RECYCLE_MULT_USDC": "2",
    "OKX_OPTIONS_CLOSE_NET_PNL_MIN_U": "0",
    "OKX_OPTIONS_CLOSE_HOLD_SECONDS": "120",
}


def _effective_env_value(key: str, file_values: dict[str, str], schema_default: str = "") -> str:
    if key == "OKX_TRADE_MODE":
        # 展示值必须与运行时 get_okx_trade_mode() 一致,避免未写入时默认 options 静默改模式
        file_val = str(file_values.get(key) or "").strip() if key in file_values else ""
        if file_val:
            from lib.hedge_plan.okx_trade_mode_lib import normalize_okx_trade_mode

            return normalize_okx_trade_mode(file_val) or file_val
        try:
            from lib.hedge_plan.okx_trade_mode_lib import get_okx_trade_mode

            return get_okx_trade_mode()
        except Exception:
            pass
    if key in file_values:
        file_val = str(file_values.get(key) or "").strip()
        if file_val:
            return file_val
    runtime = os.getenv(key)
    if runtime is not None and str(runtime).strip() != "":
        return str(runtime).strip()
    if schema_default:
        return schema_default
    return _RUNTIME_ENV_DEFAULTS.get(key, "")


def _env_truthy(raw: str) -> bool:
    return str(raw or "").strip().lower() in ("1", "true", "yes", "on")


def _schema_field_map(example_path: str) -> dict[str, dict[str, Any]]:
    out: dict[str, dict[str, Any]] = {}
    for group in parse_env_example_schema(example_path):
        for field in group.get("fields") or []:
            out[field["key"]] = dict(field)
    return out


def _build_field(
    key: str,
    label: str,
    note: str,
    schema: dict[str, dict[str, Any]],
    values: dict[str, str],
) -> dict[str, Any]:
    meta = schema.get(key) or {}
    schema_default = meta.get("default") or ""
    val = _effective_env_value(key, values, schema_default)
    # 与运行时一致:手动补开开启时,「自动平期权」展示为关闭(实际也不会执行)
    if key == "HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION":
        manual = _effective_env_value(
            "HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL", values, "true"
        )
        if _env_truthy(manual):
            val = "false"
    masked = _mask_value(key, val)
    ftype = meta.get("type") or _field_type(key, val or schema_default)
    options = select_options_for(key)
    if options:
        ftype = "select"
        val = normalize_select_value(key, val) or val
        masked = _mask_value(key, val)
    out: dict[str, Any] = {
        "key": key,
        "label": label,
        "note": note or meta.get("note") or "",
        "default": val,
        "type": ftype,
        "sensitive": meta.get("sensitive", _is_sensitive(key)),
        "restart_required": meta.get("restart_required", _restart_required(key)),
        "hot_reload": meta.get("hot_reload", _hot_reload(key)),
        "current": masked["value"] if not _is_sensitive(key) else "",
        "masked": masked["masked"],
        "tail": masked.get("tail") or "",
        "has_value": masked["has_value"],
    }
    if options:
        cur = (out["current"] or out["default"] or "").strip()
        opt_vals = {o["value"] for o in options}
        if cur and cur not in opt_vals:
            options = [{"value": cur, "label": cur}] + options
        out["options"] = options
    return out


def _okx_mode_for_env_ui() -> str:
    try:
        from lib.hedge_plan.okx_trade_mode_lib import get_okx_trade_mode

        return get_okx_trade_mode()
    except Exception:
        return "options"


def _options_fields_for_mode(mode: str) -> list[tuple[str, str, str]]:
    fields = list(_OPTIONS_SECTION["fields"])
    if mode != "options":
        fields = [f for f in fields if f[0] != "OKX_OPTIONS_MAX_ACTIVE_POSITIONS"]
    return fields


def _hedge_fields_for_mode(mode: str) -> list[tuple[str, str, str]]:
    if mode == "perp_options":
        return [*_HEDGE_COMMON_FIELDS, *_HEDGE_PO_FIELDS]
    if mode == "options_options":
        return [*_HEDGE_COMMON_FIELDS, *_HEDGE_OO_FIELDS]
    return []


def ui_sections_for_exchange(
    exchange_key: str,
    *,
    mode: str | None = None,
) -> list[dict[str, Any]]:
    ex = (exchange_key or "").strip().lower()
    sections: list[dict[str, Any]] = []
    live_fields = _EXCHANGE_LIVE_FIELDS.get(ex, _EXCHANGE_LIVE_FIELDS["okx"])
    sections.append({"title": "交易所与实盘", "fields": live_fields})
    sections.extend(_SHARED_SECTIONS)
    if ex in _MODE_SECTION.get("exchanges", frozenset()):
        from lib.hedge_plan.okx_trade_mode_lib import normalize_okx_trade_mode

        m = normalize_okx_trade_mode(mode) if mode else ""
        if not m:
            m = _okx_mode_for_env_ui()
        sections.append(_MODE_SECTION)
        sections.append({"title": "期权账户", "fields": _options_fields_for_mode(m)})
        hedge_fields = _hedge_fields_for_mode(m)
        if hedge_fields:
            title = "对冲计划·永期" if m == "perp_options" else "对冲计划·期期"
            sections.append({"title": title, "fields": hedge_fields})
    return sections


def ui_allowed_keys(exchange_key: str) -> frozenset[str]:
    """可写键=当前模式可见字段 + 模式切换键 + 遗留对冲开关(兼容旧脚本写入)."""
    keys: set[str] = set()
    for sec in ui_sections_for_exchange(exchange_key):
        for item in sec["fields"]:
            keys.add(item[0])
    ex = (exchange_key or "").strip().lower()
    if ex == "okx":
        keys.add("OKX_TRADE_MODE")
        # 允许写入遗留键,避免旧自动化/手改失败;页面不再展示
        for item in _HEDGE_PLAN_SECTION["fields"]:
            keys.add(item[0])
        for item in _OPTIONS_SECTION["fields"]:
            keys.add(item[0])
    return frozenset(keys)


def build_env_ui_payload(
    exchange_key: str,
    example_path: str,
    env_path: str,
) -> list[dict[str, Any]]:
    schema = _schema_field_map(example_path)
    env_lines = read_env_lines(env_path)
    values = env_get_all(env_lines)
    groups: list[dict[str, Any]] = []
    for sec in ui_sections_for_exchange(
        exchange_key, mode=values.get("OKX_TRADE_MODE") or ""
    ):
        fields = [
            _build_field(key, label, note, schema, values)
            for key, label, note in sec["fields"]
        ]
        fields = _mark_options_env_field_visibility(fields)
        groups.append({
            "title": sec["title"],
            "fields": fields,
            "has_restart": any(f.get("restart_required") for f in fields),
        })
    return groups


def _mark_options_env_field_visibility(fields: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """按本位/全仓复利隐藏无关项(供 SSR/前端;切换开关仍可再显示)."""
    compound_on = True
    margin_mode = "coin"
    for f in fields:
        key = f.get("key")
        cur = str(f.get("current") or f.get("default") or "").strip()
        if key == "OKX_OPTIONS_COMPOUND_FULL_ENABLED":
            compound_on = _env_truthy(cur or "true")
        elif key == "OKX_OPTIONS_MARGIN_MODE":
            margin_mode = (cur or "coin").lower()
            if margin_mode not in ("coin", "usdc"):
                margin_mode = "coin"
    out: list[dict[str, Any]] = []
    for f in fields:
        item = dict(f)
        key = item.get("key")
        hide = False
        if key == "OKX_OPTIONS_TRADE_BUDGET_USDC" and compound_on:
            hide = True
        if key == "OKX_OPTIONS_CLOSE_RECYCLE_MULT_USDC" and margin_mode == "coin":
            hide = True
        if key == "OKX_OPTIONS_CLOSE_RECYCLE_MULT_COIN" and margin_mode == "usdc":
            hide = True
        if hide:
            item["hidden"] = True
        out.append(item)
    return out


def _mark_compound_budget_hidden(fields: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """兼容旧调用名;实际走 _mark_options_env_field_visibility."""
    return _mark_options_env_field_visibility(fields)

def filter_updates_for_ui(exchange_key: str, updates: dict[str, str]) -> dict[str, str]:
    allowed = ui_allowed_keys(exchange_key)
    return {k: v for k, v in (updates or {}).items() if k in allowed}


def validate_env_ui_updates(
    exchange_key: str,
    example_path: str,
    updates: dict[str, str],
) -> tuple[dict[str, str], list[str]]:
    from lib.env.env_schema import validate_env_updates

    schema = _schema_field_map(example_path)
    groups: list[dict[str, Any]] = []
    for sec in ui_sections_for_exchange(exchange_key):
        fields: list[dict[str, Any]] = []
        for key, _label, _note in sec["fields"]:
            if key in schema:
                field = dict(schema[key])
                opts = select_options_for(key)
                if opts:
                    field["type"] = "select"
                    field["options"] = opts
                fields.append(field)
            else:
                default = ""
                fields.append(
                    {
                        "key": key,
                        "type": _field_type(key, default),
                        "sensitive": _is_sensitive(key),
                        "restart_required": _restart_required(key),
                        "hot_reload": _hot_reload(key),
                        "options": select_options_for(key),
                    }
                )
        groups.append({"title": sec["title"], "fields": fields})
    return validate_env_updates(groups, updates)


def coerce_hedge_partial_close_with_manual(
    clean: dict[str, str],
    *,
    env_path: str = "",
) -> dict[str, str]:
    """手动补开为开启时,强制把自动平写成 false(与运行时一致)."""
    out = dict(clean or {})
    manual = out.get("HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL")
    if manual is None and env_path:
        try:
            from lib.env.env_file_lib import env_get_all, read_env_lines

            file_vals = env_get_all(read_env_lines(env_path))
            manual = _effective_env_value(
                "HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL", file_vals, "true"
            )
        except Exception:
            manual = "true"
    if _env_truthy(str(manual or "")):
        out["HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION"] = "false"
    return out
