import { stageOptions } from "@/features/lucky-gift/constants.js";
import {
    applyProbabilityMap,
    correctRTPProbabilities,
    decimal,
    expectedRTPPercent,
    formatDecimal,
    probabilityTotal,
} from "@/shared/utils/rtpProbability.js";
import { DEFAULT_POOL_ID } from "./api.js";

const DYNAMIC_STRATEGY = "dynamic_v3";
const PPM_SCALE = 1_000_000;
const LEGACY_JACKPOT_RTP_MODE = "__legacy_jackpot_rtp_fields";
const LEGACY_USER_24H_RTP_MODE = "__legacy_user_24h_rtp_fields";
const USER_24H_RTP_DEFAULT_THRESHOLD_PERCENT = 150;
const USER_24H_RTP_DISABLED_FACTOR_PERCENT = 100;
const legacyJackpotRTPAliases = {
    jackpot_user_48h_rtp_max_percent: [
        "jackpot_user_72h_rtp_max_percent",
        "jackpotUser72hRTPMaxPercent",
        "jackpotUser72hRtpMaxPercent",
    ],
    jackpot_user_round_rtp_max_percent: [
        "jackpot_user_day_rtp_max_percent",
        "jackpotUserDayRTPMaxPercent",
        "jackpotUserDayRtpMaxPercent",
    ],
};
const user24hRTPAliases = {
    user_24h_ordinary_win_factor_percent: ["user24HOrdinaryWinFactorPercent"],
    user_24h_rtp_threshold_percent: ["user24HRTPThresholdPercent", "user24hRTPThresholdPercent"],
};

// 只有跨 App 通用的算法参数才有默认值；六维金额上限必须由运营按 App 奖池的金币口径填写，
// 因而默认保持 0 且规则停用，避免把某个 App 的预算误发到其他 App。
const dynamicDefaults = {
    anchor_daily_payout_cap: 0,
    anchor_rate_percent: 1,
    control_band_percent: 3,
    device_daily_payout_cap: 0,
    high_water_nonzero_factor_percent: 130,
    high_watermark_coins: 20_000_000,
    // dynamic_v3 的运行资金只能通过带审计的奖池水位接口注入，发布规则不再隐式 seed。
    initial_pool_coins: 0,
    jackpot_global_rtp_max_percent: 98,
    jackpot_multipliers: [200, 500, 1000],
    jackpot_spend_threshold_coins: 0,
    jackpot_user_48h_rtp_max_percent: 96,
    jackpot_user_round_rtp_max_percent: 96,
    loss_streak_guarantee: 5,
    low_water_nonzero_factor_percent: 70,
    low_watermark_coins: 10_000_000,
    max_jackpot_hits_per_user_day: 5,
    max_single_payout: 0,
    pool_rate_percent: 98,
    profit_rate_percent: 1,
    recharge_boost_factor_percent: 110,
    recharge_boost_window_ms: 300_000,
    room_hourly_payout_cap: 0,
    target_rtp_percent: 98,
    user_24h_ordinary_win_factor_percent: 70,
    user_24h_rtp_threshold_percent: USER_24H_RTP_DEFAULT_THRESHOLD_PERCENT,
    user_daily_payout_cap: 0,
    user_hourly_payout_cap: 0,
};

const numericFields = [
    "anchor_daily_payout_cap",
    "anchor_rate_percent",
    "control_band_percent",
    "device_daily_payout_cap",
    "effective_from_ms",
    "gift_price_reference",
    "high_water_nonzero_factor_percent",
    "high_watermark_coins",
    "initial_pool_coins",
    "jackpot_global_rtp_max_percent",
    "jackpot_spend_threshold_coins",
    "jackpot_user_48h_rtp_max_percent",
    "jackpot_user_round_rtp_max_percent",
    "loss_streak_guarantee",
    "low_water_nonzero_factor_percent",
    "low_watermark_coins",
    "max_jackpot_hits_per_user_day",
    "max_single_payout",
    "normal_max_equivalent_draws",
    "novice_max_equivalent_draws",
    "pool_rate_percent",
    "profit_rate_percent",
    "recharge_boost_factor_percent",
    "recharge_boost_window_ms",
    "room_hourly_payout_cap",
    "settlement_window_wager",
    "target_rtp_percent",
    "user_24h_ordinary_win_factor_percent",
    "user_24h_rtp_threshold_percent",
    "user_daily_payout_cap",
    "user_hourly_payout_cap",
    "v2_high_water_boost_factor_percent",
    "v2_high_water_boost_recover_coins",
    "v2_high_water_boost_threshold_coins",
];

const payoutCapFields = [
    ["max_single_payout", "单次返奖上限"],
    ["user_hourly_payout_cap", "用户小时上限"],
    ["user_daily_payout_cap", "用户每日上限"],
    ["device_daily_payout_cap", "设备每日上限"],
    ["room_hourly_payout_cap", "房间小时上限"],
    ["anchor_daily_payout_cap", "主播每日上限"],
];

export function configToForm(config = {}) {
    const hasPersistedShape = Object.keys(config).length > 0;
    const strategyVersion = stringConfigValue(
        config,
        "strategy_version",
        "strategyVersion",
        hasPersistedShape ? "fixed_v2" : DYNAMIC_STRATEGY,
    );
    const targetRTPPercent = formConfigNumber(
        config,
        "target_rtp_percent",
        "targetRtpPercent",
        strategyVersion === DYNAMIC_STRATEGY ? dynamicDefaults.target_rtp_percent : 95,
    );
    const form = {
        app_code: config.app_code || config.appCode || "",
        enabled: Boolean(config.enabled),
        jackpot_multipliers: multiplierListToForm(
            config.jackpot_multipliers ?? config.jackpotMultipliers,
            strategyVersion,
        ),
        pool_id: config.pool_id || config.poolId || DEFAULT_POOL_ID,
        stages: normalizeFormStages(config.stages, targetRTPPercent, strategyVersion),
        strategy_version: strategyVersion,
    };
    const newDynamicDraft = strategyVersion === DYNAMIC_STRATEGY && isNewDynamicDraft(config);
    // 只在明确收到旧 day/72h 且缺少对应 canonical 字段时进入兼容模式。页面始终编辑 canonical 值，
    // 这个标记仅让纯函数保留旧对象形状；HTTP 层仍会归一成 round/48h 后再发送。
    form[LEGACY_JACKPOT_RTP_MODE] = Object.entries(legacyJackpotRTPAliases).some(
        ([canonical, aliases]) =>
            !hasConfigValue(config, [canonical, snakeToCamel(canonical)]) && hasConfigValue(config, aliases),
    );
    // 已发布的旧规则没有近 24 小时降权字段，或者以 0/100 明确表示关闭。编辑时把禁用态展示为
    // 更容易理解的 150/100，但保留来源形状：运营不改这两个值就不会意外给历史版本启用 70% 降权。
    form[LEGACY_USER_24H_RTP_MODE] = legacyUser24hRTPMode(config, strategyVersion);
    for (const field of numericFields) {
        if (field === "target_rtp_percent") {
            form[field] = targetRTPPercent;
            continue;
        }
        if (field === "initial_pool_coins" && strategyVersion === DYNAMIC_STRATEGY) {
            // 旧 dynamic_v3 快照可能仍保存 1,000,000；新增版本必须显式归零，不能把历史隐式注资语义继续复制。
            form[field] = "0";
            continue;
        }
        if (newDynamicDraft && field === "user_24h_rtp_threshold_percent") {
            // 兼容 admin-server 先于本字段升级时返回的显式 0/100 草稿；未发布版本始终采用新建默认。
            form[field] = String(USER_24H_RTP_DEFAULT_THRESHOLD_PERCENT);
            continue;
        }
        if (newDynamicDraft && field === "user_24h_ordinary_win_factor_percent") {
            form[field] = String(dynamicDefaults.user_24h_ordinary_win_factor_percent);
            continue;
        }
        if (form[LEGACY_USER_24H_RTP_MODE] && field === "user_24h_rtp_threshold_percent") {
            form[field] = String(USER_24H_RTP_DEFAULT_THRESHOLD_PERCENT);
            continue;
        }
        if (form[LEGACY_USER_24H_RTP_MODE] && field === "user_24h_ordinary_win_factor_percent") {
            form[field] = String(USER_24H_RTP_DISABLED_FACTOR_PERCENT);
            continue;
        }
        const aliases = [...(legacyJackpotRTPAliases[field] || []), ...(user24hRTPAliases[field] || [])];
        form[field] = hasConfigValue(config, [field, snakeToCamel(field), ...aliases])
            ? formNumber(firstConfigValue(config, [field, snakeToCamel(field), ...aliases]))
            : formNumber(defaultNumber(field, strategyVersion));
    }
    if (form[LEGACY_JACKPOT_RTP_MODE]) {
        form.jackpot_user_72h_rtp_max_percent = form.jackpot_user_48h_rtp_max_percent;
        form.jackpot_user_day_rtp_max_percent = form.jackpot_user_round_rtp_max_percent;
    }
    return form;
}

export function formToConfig(config, form) {
    const output = {
        ...config,
        app_code: form.app_code,
        enabled: Boolean(form.enabled),
        jackpot_multipliers: multiplierListFromForm(form.jackpot_multipliers),
        pool_id: form.pool_id,
        stages: (form.stages || []).map((stage) => {
            // dynamic_v3 的新手范围由普通阶段下限反向派生；0/0 只是旧 DTO 要求的兜底哨兵值，
            // 不是可运营的“充值为 0 才是新手”规则。序列化时强制归零，避免历史异常值改变分层。
            const isDynamicNovice = form.strategy_version === DYNAMIC_STRATEGY && stage.stage === "novice";
            return {
                min_recharge_30d_coins: isDynamicNovice ? 0 : numberFromForm(stage.min_recharge_30d_coins),
                min_recharge_7d_coins: isDynamicNovice ? 0 : numberFromForm(stage.min_recharge_7d_coins),
                stage: stage.stage,
                tiers: (stage.tiers || []).map((tier) => ({
                    enabled: tier.enabled !== false,
                    high_water_only: Boolean(tier.highWaterOnly),
                    multiplier: numberFromForm(tier.multiplier),
                    probability_percent: numberFromForm(tier.probabilityPercent),
                    tier_id: tier.tierId || "",
                })),
            };
        }),
        strategy_version: form.strategy_version,
    };
    for (const field of numericFields) {
        // dynamic_v3 强制归零：隐式初始注资已废弃，V2 高水位加权字段属于 fixed_v2 专属，owner 会拒绝混填。
        const forcedZero =
            form.strategy_version === DYNAMIC_STRATEGY &&
            (field === "initial_pool_coins" || field.startsWith("v2_high_water_boost"));
        output[field] = forcedZero ? 0 : numberFromForm(form[field]);
    }
    if (
        form.strategy_version === DYNAMIC_STRATEGY &&
        form[LEGACY_USER_24H_RTP_MODE] &&
        numberFromForm(form.user_24h_rtp_threshold_percent) === USER_24H_RTP_DEFAULT_THRESHOLD_PERCENT &&
        numberFromForm(form.user_24h_ordinary_win_factor_percent) === USER_24H_RTP_DISABLED_FACTOR_PERCENT
    ) {
        // 缺字段的旧快照继续保持原始 shape，显式 0/100 的旧快照则继续写回禁用值；两者再次发布都无效果。
        if (form[LEGACY_USER_24H_RTP_MODE] === "missing") {
            delete output.user_24h_rtp_threshold_percent;
            delete output.user_24h_ordinary_win_factor_percent;
        } else {
            output.user_24h_rtp_threshold_percent = 0;
            output.user_24h_ordinary_win_factor_percent = USER_24H_RTP_DISABLED_FACTOR_PERCENT;
        }
    }
    if (form[LEGACY_JACKPOT_RTP_MODE]) {
        // 旧输入的纯函数 round-trip 保持原 shape，避免调用方只做本地比较时出现伪 diff；真正保存时
        // luckyGiftConfigPayload 会把这两个别名归一成 canonical HTTP 字段，绝不会双发两套字段。
        output.jackpot_user_72h_rtp_max_percent = output.jackpot_user_48h_rtp_max_percent;
        output.jackpot_user_day_rtp_max_percent = output.jackpot_user_round_rtp_max_percent;
        delete output.jackpot_user_48h_rtp_max_percent;
        delete output.jackpot_user_round_rtp_max_percent;
    } else {
        delete output.jackpot_user_72h_rtp_max_percent;
        delete output.jackpot_user_day_rtp_max_percent;
    }
    return output;
}

export function giftTypeForLuckyGiftPool(poolID) {
    // room/wallet 把任意非空幸运奖池归入 lucky，只有 super_lucky 家族使用超级幸运礼物比例；
    // 测试池和外部自定义池因此沿用普通幸运礼物口径，不要求运营额外维护一份映射。
    return String(poolID || "")
        .trim()
        .toLowerCase()
        .includes("super_lucky")
        ? "super_lucky"
        : "lucky";
}

export function giftHostDiamondRatioForPool(ratios, poolID) {
    const value = Number(ratios?.[giftTypeForLuckyGiftPool(poolID)]);
    return Number.isFinite(value) && value >= 0 && value <= 100 ? value : null;
}

export function applyGiftHostDiamondRatio(form, ratio, { rebalance = false } = {}) {
    if (!Number.isFinite(ratio) || ratio < 0 || ratio > 100) return form;
    const next = { ...form, anchor_rate_percent: formNumber(ratio) };
    if (!rebalance || next.strategy_version !== DYNAMIC_STRATEGY) return next;

    // anchor_rate_percent 是历史协议名；V3 实际使用 wallet 的主播钻石事实，不读取收礼返币。
    // 新奖池只把可配置的平台比例和接口返回的主播钻石预留扣除，剩余资金进入公共奖池。
    const availablePoolRate = roundedPercent(100 - numberFromForm(next.profit_rate_percent) - ratio);
    next.pool_rate_percent = formNumber(Math.max(availablePoolRate, 0));
    next.target_rtp_percent = formNumber(
        Math.min(numberFromForm(next.target_rtp_percent), Math.max(availablePoolRate, 0)),
    );
    return correctAllStageProbabilities(next);
}

// 从历史 fixed_v2 显式切换到 dynamic_v3 时，先停用并载入安全草稿；金额型上限仍保持 0，
// 运营补齐后才能通过启用校验，避免一次策略切换直接发布无法结算的规则。
export function applyDynamicStrategyDefaults(form, { anchorRatePercent = null } = {}) {
    const next = {
        ...form,
        enabled: false,
        jackpot_multipliers: dynamicDefaults.jackpot_multipliers.join(", "),
        strategy_version: DYNAMIC_STRATEGY,
    };
    for (const [field, value] of Object.entries(dynamicDefaults)) {
        if (field !== "jackpot_multipliers") {
            next[field] = String(value);
        }
    }
    if (Number.isFinite(anchorRatePercent) && anchorRatePercent >= 0 && anchorRatePercent <= 100) {
        next.anchor_rate_percent = formNumber(anchorRatePercent);
        const availablePoolRate = Math.max(
            roundedPercent(100 - numberFromForm(next.profit_rate_percent) - anchorRatePercent),
            0,
        );
        next.pool_rate_percent = formNumber(availablePoolRate);
        next.target_rtp_percent = formNumber(Math.min(dynamicDefaults.target_rtp_percent, availablePoolRate));
    }
    next.stages = stageOptions.map(([stage]) => {
        const [min7d, min30d] = defaultRechargeThreshold(stage);
        return {
            min_recharge_7d_coins: String(min7d),
            min_recharge_30d_coins: String(min30d),
            stage,
            // fixed_v2 可能含历史高倍基础档；切换策略时必须按当前资金可用 RTP 重建安全基线，
            // 不能只改策略名后把旧概率意外带入 dynamic_v3 的第一版。
            tiers: correctedDefaultTiers(stage, numberFromForm(next.target_rtp_percent)),
        };
    });
    return next;
}

// 概率列必须整体闭合到 100% 且期望 RTP 逼近目标值，所以任何倍率/目标 RTP 改动后都重算全阶段概率，
// 不允许运营手工填概率绕过服务端的闭合校验。
export function correctAllStageProbabilities(form) {
    return {
        ...form,
        stages: (form.stages || []).map((stage) => {
            const probabilityByIndex = correctRTPProbabilities(stage.tiers, form.target_rtp_percent, {
                multiplier: (item) => item.multiplier,
            });
            return {
                ...stage,
                tiers: probabilityByIndex
                    ? applyProbabilityMap(stage.tiers, probabilityByIndex, formatDecimal)
                    : stage.tiers,
            };
        }),
    };
}

export function stageSummary(stage, form) {
    const probability = probabilityTotal(stage.tiers);
    const expected = expectedRTPPercent(stage.tiers, { multiplier: (item) => item.multiplier });
    const target = Number(form.target_rtp_percent || 0);
    const band = Number(form.control_band_percent || 0);
    const probabilityClosed = Math.abs(probability - 100) < 0.0001;
    const rtpInBand = expected >= target - band && expected <= target + band;
    const fullLabel = stageOptions.find(([key]) => key === stage.stage)?.[1] || stage.stage;
    return {
        expected,
        ok: probabilityClosed && rtpInBand,
        probability,
        probabilityClosed,
        rtpInBand,
        stageKey: stage.stage,
        stageLabel: fullLabel,
        status: probabilityClosed && rtpInBand ? "可发布" : probabilityClosed ? "RTP 偏离" : "概率未闭合",
        tabLabel: stage.stage === "normal" ? "普通" : fullLabel.replace(/阶段$/, ""),
    };
}

export function validateLuckyGiftForm(form) {
    const errors = [];
    const number = (field) => numberFromForm(form[field]);
    if (!String(form.app_code || "").trim()) errors.push("请选择应用");
    if (!String(form.pool_id || "").trim()) errors.push("奖池不能为空");
    if (!["fixed_v2", DYNAMIC_STRATEGY].includes(form.strategy_version)) errors.push("策略版本不受支持");
    if (number("target_rtp_percent") < 10 || number("target_rtp_percent") > 99)
        errors.push("目标 RTP 必须在 10%-99% 之间");
    if (number("pool_rate_percent") < number("target_rtp_percent") || number("pool_rate_percent") > 100)
        errors.push("奖池比例必须在目标 RTP 与 100% 之间");
    if (number("control_band_percent") <= 0 || number("control_band_percent") > 5)
        errors.push("控制带宽必须大于 0 且不超过 5%");
    if (number("settlement_window_wager") <= 0) errors.push("结算窗口流水必须大于 0");
    if (number("gift_price_reference") <= 0) errors.push("礼物参考金额必须大于 0");
    if (
        number("novice_max_equivalent_draws") < 0 ||
        number("normal_max_equivalent_draws") < number("novice_max_equivalent_draws")
    )
        errors.push("等价抽数阶段门槛不正确");

    // 阶段概率与 RTP 是两代策略共用的 owner 校验，fixed_v2 也不能跳过。
    for (const stageKey of ["novice", "normal", "advanced"]) {
        const stage = form.stages?.find((item) => item.stage === stageKey);
        if (!stage) {
            errors.push("必须配置新手、正常和高阶三个阶段");
            continue;
        }
        const enabled = stage.tiers.filter((tier) => tier.enabled !== false);
        if (enabled.some((tier) => numberFromForm(tier.multiplier) < 0))
            errors.push(`${stageLabel(stageKey)}倍率不能小于 0`);
        const probabilityPPM = enabled.reduce((sum, tier) => sum + percentToPPM(tier.probabilityPercent), 0);
        if (probabilityPPM !== PPM_SCALE) errors.push(`${stageLabel(stageKey)}启用奖档概率必须合计 100%`);
        if (!enabled.some((tier) => Number(tier.multiplier) === 0))
            errors.push(`${stageLabel(stageKey)}必须包含启用的 0x 档`);
        if (enabled.some((tier) => Number(tier.probabilityPercent) <= 0))
            errors.push(`${stageLabel(stageKey)}启用奖档概率必须大于 0`);
        if (enabled.some((tier) => Number(tier.multiplier) >= 10 && !tier.highWaterOnly))
            errors.push(`${stageLabel(stageKey)}10x 及以上奖档必须限制为高水位`);
        const expected = expectedRTPPercent(enabled, { multiplier: (item) => item.multiplier });
        if (
            expected < number("target_rtp_percent") - number("control_band_percent") ||
            expected > number("target_rtp_percent") + number("control_band_percent")
        )
            errors.push(`${stageLabel(stageKey)}期望 RTP 超出控制带宽`);
    }

    if (form.strategy_version !== DYNAMIC_STRATEGY) {
        // V2 高水位加权三元组：阈值 0 表示关闭且另两项必须为 0；启用时系数放大 RTP，恢复水位构成滞回下沿。
        const boostThreshold = number("v2_high_water_boost_threshold_coins");
        const boostFactor = number("v2_high_water_boost_factor_percent");
        const boostRecover = number("v2_high_water_boost_recover_coins");
        if (boostThreshold === 0) {
            if (boostFactor !== 0 || boostRecover !== 0)
                errors.push("关闭 V2 高水位加权时，加权系数与恢复水位必须为 0");
        } else {
            if (boostThreshold < 0) errors.push("V2 高水位加权阈值不能小于 0");
            if (boostFactor <= 100 || boostFactor > 1000) errors.push("V2 高水位加权系数必须大于 100% 且不超过 1000%");
            if (boostRecover <= 0 || boostRecover > boostThreshold)
                errors.push("V2 加权恢复水位必须大于 0 且不高于加权阈值");
        }
        return unique(errors);
    }

    if (number("profit_rate_percent") < 0 || number("anchor_rate_percent") < 0)
        errors.push("平台比例和主播钻石比例不能小于 0");
    if (
        percentToPPM(number("pool_rate_percent")) +
            percentToPPM(number("profit_rate_percent")) +
            percentToPPM(number("anchor_rate_percent")) !==
        PPM_SCALE
    )
        errors.push("奖池、平台和主播钻石比例合计必须等于 100%");
    if (number("initial_pool_coins") !== 0) errors.push("dynamic_v3 初始奖池必须为 0，请通过水位操作注资");
    if (number("loss_streak_guarantee") <= 0) errors.push("连续未中奖保护次数必须大于 0");
    if (number("low_watermark_coins") <= 0 || number("high_watermark_coins") <= number("low_watermark_coins"))
        errors.push("高水位必须大于正数低水位");
    if (number("low_water_nonzero_factor_percent") <= 0 || number("low_water_nonzero_factor_percent") >= 100)
        errors.push("低水位非零因子必须大于 0% 且小于 100%");
    if (number("high_water_nonzero_factor_percent") <= 100) errors.push("高水位非零因子必须大于 100%");
    if (number("recharge_boost_window_ms") <= 0 || number("recharge_boost_factor_percent") <= 100)
        errors.push("充值加权窗口必须大于 0 且因子必须大于 100%");
    if (number("user_24h_rtp_threshold_percent") <= 0) errors.push("用户近 24 小时 RTP 触发线必须大于 0%");
    if (number("user_24h_ordinary_win_factor_percent") <= 0 || number("user_24h_ordinary_win_factor_percent") > 100)
        errors.push("高 RTP 时普通中奖机会保留比例必须大于 0% 且不超过 100%");
    const jackpotTokens = String(form.jackpot_multipliers || "")
        .split(/[,，\s]+/)
        .filter(Boolean);
    const jackpotMultiplierValues = multiplierListFromForm(form.jackpot_multipliers);
    if (
        jackpotTokens.length !== jackpotMultiplierValues.length ||
        !jackpotMultiplierValues.length ||
        jackpotMultiplierValues.some(
            (value, index) => value <= 1 || (index > 0 && value <= jackpotMultiplierValues[index - 1]),
        )
    ) {
        errors.push("大奖倍率必须大于 1x 并严格递增");
    }
    if (number("jackpot_global_rtp_max_percent") <= 0 || number("jackpot_global_rtp_max_percent") > 100)
        errors.push("全局大奖 RTP 上限必须在 0%-100% 之间");
    if (
        number("jackpot_user_round_rtp_max_percent") <= 0 ||
        number("jackpot_user_round_rtp_max_percent") > number("jackpot_global_rtp_max_percent")
    )
        errors.push("用户本轮 RTP 触发线必须大于 0 且不超过全局上限");
    if (
        number("jackpot_user_48h_rtp_max_percent") <= 0 ||
        number("jackpot_user_48h_rtp_max_percent") > number("jackpot_global_rtp_max_percent")
    )
        errors.push("用户 48 小时 RTP 触发线必须大于 0 且不超过全局上限");
    if (number("max_jackpot_hits_per_user_day") <= 0) errors.push("用户每日大奖次数必须大于 0");
    if (number("jackpot_spend_threshold_coins") < 0) errors.push("大奖消费门槛不能小于 0");
    // 历史版本可以继续读取和展示 0；只有运营要发布为启用状态时，才要求为重新启用的消费策略
    // 提供正数门槛。该判断不依赖旧字段别名，避免 canonical 新版本绕过校验直接上线。
    if (form.strategy_version === DYNAMIC_STRATEGY && form.enabled && number("jackpot_spend_threshold_coins") <= 0)
        errors.push("启用前必须填写用户日累计消费触发门槛（金币）");
    for (const [field, label] of payoutCapFields) {
        if (number(field) < 0) errors.push(`${label}不能小于 0`);
    }
    validateRechargeStages(form.stages || [], errors, form.enabled);
    if (form.enabled) {
        for (const [field, label] of payoutCapFields) {
            if (number(field) <= 0) errors.push(`启用前必须填写${label}`);
        }
    }
    return unique(errors);
}

export function nextStageMultiplier(tiers = []) {
    const positive = tiers.map((tier) => decimal(tier.multiplier)).filter((value) => value > 0);
    if (!positive.length) return 1;
    const max = Math.max(...positive);
    if (max >= 5) return max * 2;
    if (max >= 2) return 5;
    return max + 1;
}

function normalizeFormStages(stages, targetRTPPercent, strategyVersion) {
    const byStage = new Map((Array.isArray(stages) ? stages : []).map((stage) => [stage.stage, stage]));
    return stageOptions.map(([stageKey]) => {
        const source = byStage.get(stageKey);
        const sourceTiers =
            Array.isArray(source?.tiers) && source.tiers.length
                ? source.tiers
                : correctedDefaultTiers(stageKey, targetRTPPercent);
        const [default7d, default30d] =
            strategyVersion === DYNAMIC_STRATEGY ? defaultRechargeThreshold(stageKey) : [0, 0];
        const isDynamicNovice = strategyVersion === DYNAMIC_STRATEGY && stageKey === "novice";
        return {
            // 只在表单内保留兼容哨兵值；界面上的新手上界始终从普通门槛派生，不读取历史 novice 门槛。
            min_recharge_7d_coins: isDynamicNovice
                ? "0"
                : formNumber(source?.min_recharge_7d_coins ?? source?.minRecharge7DCoins ?? default7d),
            min_recharge_30d_coins: isDynamicNovice
                ? "0"
                : formNumber(source?.min_recharge_30d_coins ?? source?.minRecharge30DCoins ?? default30d),
            stage: stageKey,
            tiers: sourceTiers.map((tier) => ({
                enabled: tier.enabled !== false,
                highWaterOnly: Boolean(tier.high_water_only ?? tier.highWaterOnly),
                multiplier: formNumber(tier.multiplier),
                probabilityPercent: formNumber(tier.probability_percent ?? tier.probabilityPercent),
                tierId: tier.tier_id || tier.tierId || "",
            })),
        };
    });
}

function correctedDefaultTiers(stage, targetRTPPercent) {
    const tiers = [
        defaultTier(`${stage}_none`, 0, 5),
        defaultTier(`${stage}_0_5x`, 0.5, 4),
        defaultTier(`${stage}_1x`, 1, 86),
        defaultTier(`${stage}_2x`, 2, 5),
    ];
    // dynamic_v3 owner 的 98% 安全基线不只是数学 RTP，还规定了 0x/0.5x/1x/2x 的体验节奏。
    // 这组 5/4/86/5 本身已精确达到 98%，不能再交给无 baseline 的通用求解器重排成另一组同 RTP 概率。
    if (Math.abs(numberFromForm(targetRTPPercent) - dynamicDefaults.target_rtp_percent) < 0.0001) {
        return tiers;
    }
    const probabilities = correctRTPProbabilities(tiers, targetRTPPercent, { multiplier: (item) => item.multiplier });
    return probabilities ? applyProbabilityMap(tiers, probabilities, formatDecimal) : tiers;
}

function defaultTier(tierID, multiplier, probabilityPercent) {
    return { enabled: true, highWaterOnly: false, multiplier, probabilityPercent, tierId: tierID };
}

function validateRechargeStages(stages, errors, requireComplete) {
    const normal = stages.find((stage) => stage.stage === "normal");
    const advanced = stages.find((stage) => stage.stage === "advanced");
    const threshold = (stage, field) => numberFromForm(stage?.[field]);
    const normal7d = threshold(normal, "min_recharge_7d_coins");
    const normal30d = threshold(normal, "min_recharge_30d_coins");
    const advanced7d = threshold(advanced, "min_recharge_7d_coins");
    const advanced30d = threshold(advanced, "min_recharge_30d_coins");
    if ([normal7d, normal30d, advanced7d, advanced30d].some((value) => value < 0)) {
        errors.push("充值阶段门槛不能小于 0");
        return;
    }
    const isUnconfigured = [normal7d, normal30d, advanced7d, advanced30d].every((value) => value === 0);
    // 新建的停用草稿可以全 0，避免猜测 App 金币价值；一旦开始填写或准备启用，
    // 就必须遵守 owner 的二维下限契约：normal 至少一维大于 0，advanced 逐维不低且至少一维更高。
    if (!requireComplete && isUnconfigured) return;
    if (normal7d === 0 && normal30d === 0)
        errors.push(
            requireComplete ? "启用前普通阶段至少一个最低充值门槛必须大于 0" : "普通阶段至少一个充值门槛必须大于 0",
        );
    if (advanced7d < normal7d || advanced30d < normal30d || (advanced7d === normal7d && advanced30d === normal30d))
        errors.push("高阶充值门槛必须逐维不低于正常阶段且至少一项更高");
}

function defaultRechargeThreshold() {
    // 充值门槛受 App 金币比例影响，新草稿不猜测 1 金币等于可用配置；运营填完后才能启用。
    return [0, 0];
}

function defaultNumber(field, strategyVersion) {
    if (field === "gift_price_reference") return 100;
    if (field === "settlement_window_wager") return 1_000_000;
    if (field === "novice_max_equivalent_draws") return 2_000;
    if (field === "normal_max_equivalent_draws") return 20_000;
    if (field === "effective_from_ms") return 0;
    return strategyVersion === DYNAMIC_STRATEGY ? (dynamicDefaults[field] ?? 0) : 0;
}

function legacyUser24hRTPMode(config, strategyVersion) {
    if (strategyVersion !== DYNAMIC_STRATEGY) return "";
    // rule_version=0 / is_default 是尚未发布的草稿，即使旧后端显式返回 0/100 也必须使用新建
    // 默认；只有已发布版本的缺字段或 0/100 才属于历史禁用兼容。
    if (isNewDynamicDraft(config)) return "";
    const threshold = firstConfigValue(config, [
        "user_24h_rtp_threshold_percent",
        snakeToCamel("user_24h_rtp_threshold_percent"),
        ...user24hRTPAliases.user_24h_rtp_threshold_percent,
    ]);
    const factor = firstConfigValue(config, [
        "user_24h_ordinary_win_factor_percent",
        snakeToCamel("user_24h_ordinary_win_factor_percent"),
        ...user24hRTPAliases.user_24h_ordinary_win_factor_percent,
    ]);
    if (threshold === undefined && factor === undefined) return "missing";
    if (Number(threshold || 0) === 0 && [undefined, 0, 100].includes(factor === undefined ? undefined : Number(factor)))
        return "disabled";
    return "";
}

function isNewDynamicDraft(config) {
    if (config.is_default === true || config.isDefault === true) return true;
    if (hasConfigValue(config, ["rule_version", "ruleVersion"])) {
        return Number(firstConfigValue(config, ["rule_version", "ruleVersion"])) <= 0;
    }
    // 新建入口只传规则身份；若已经带有资金、概率或水位字段，就按旧发布快照处理，不能套 70% 新默认值。
    const identityKeys = new Set([
        "app_code",
        "appCode",
        "enabled",
        "pool_id",
        "poolId",
        "strategy_version",
        "strategyVersion",
    ]);
    return Object.keys(config).every((key) => identityKeys.has(key));
}

function multiplierListToForm(value, strategyVersion) {
    if (Array.isArray(value) && value.length) return value.map(formatDecimal).join(", ");
    if (typeof value === "string" && value.trim()) return value;
    return strategyVersion === DYNAMIC_STRATEGY ? dynamicDefaults.jackpot_multipliers.join(", ") : "";
}

function multiplierListFromForm(value) {
    return String(value || "")
        .split(/[,，\s]+/)
        .filter(Boolean)
        .map(Number)
        .filter(Number.isFinite);
}

function stringConfigValue(config, snake, camel, fallback) {
    const value = config[snake] ?? config[camel];
    return String(value || fallback);
}

function formConfigNumber(config, snake, camel, fallback) {
    return formNumber(config[snake] ?? config[camel] ?? fallback);
}

function firstConfigValue(config, keys) {
    for (const key of keys) {
        if (config[key] !== undefined && config[key] !== null) return config[key];
    }
    return undefined;
}

function hasConfigValue(config, keys) {
    return firstConfigValue(config, keys) !== undefined;
}

function snakeToCamel(value) {
    return value.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase());
}

function stageLabel(stage) {
    return stageOptions.find(([key]) => key === stage)?.[1] || stage;
}

function percentToPPM(value) {
    return Math.round(Number(value || 0) * 10_000);
}

function roundedPercent(value) {
    return Math.round(Number(value || 0) * 10_000) / 10_000;
}

function unique(values) {
    return Array.from(new Set(values));
}

export function formNumber(value) {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? String(parsed) : "0";
}

export function numberFromForm(value) {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : 0;
}
