var test_api = 'https://api-test.global-interaction.com/';
var local_api = 'http://127.0.0.1:13000/';
var default_api = 'https://api.global-interaction.com/';

(function () {
    var API_ENV_KEY = 'hyapp_h5_env';
    var ACCESS_TOKEN_KEY = 'hyapp_access_token';
    var APP_CODE_KEY = 'hyapp_app_code';
    var RENEWED_ACCESS_TOKEN_HEADER = 'X-Hyapp-Access-Token';
    var LANGUAGE_QUERY_KEYS = [
        'language',
        'lang',
        'locale',
        'app_lang',
        'appLanguage',
    ];
    var memoryEnv = '';
    var memoryAccessToken = '';
    var memoryAppCode = '';
    var activeAppConfig = null;

    function currentQueryParams() {
        var params = new URLSearchParams(window.location.search);
        var hash = window.location.hash || '';
        if (hash.indexOf('?') >= 0) {
            new URLSearchParams(hash.split('?').slice(1).join('?')).forEach(
                function (value, key) {
                    if (!params.has(key)) params.set(key, value);
                }
            );
        }
        return params;
    }

    function readQuery(name) {
        return currentQueryParams().get(name);
    }

    function readLanguageQuery() {
        var params = currentQueryParams();
        for (var i = 0; i < LANGUAGE_QUERY_KEYS.length; i += 1) {
            var value = params.get(LANGUAGE_QUERY_KEYS[i]);
            if (value) return value;
        }
        return '';
    }

    function normalizeToken(value) {
        var token = String(value || '').trim();
        if (!token) return '';
        if (/^Bearer\s+/i.test(token)) token = token.replace(/^Bearer\s+/i, '');
        var match = token.match(
            /[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/
        );
        return match ? match[0] : token;
    }

    function decodeBase64URLText(value) {
        try {
            var normalized = String(value || '')
                .replace(/-/g, '+')
                .replace(/_/g, '/');
            while (normalized.length % 4) normalized += '=';
            return window.atob(normalized);
        } catch (_) {
            return '';
        }
    }

    function decodeBase64URLJSON(value) {
        try {
            var text = decodeBase64URLText(value);
            return text ? JSON.parse(text) : null;
        } catch (_) {
            return null;
        }
    }

    function numericUserID(value) {
        var text = String(value || '').trim();
        return /^\d{6,}$/.test(text) ? text : '';
    }

    function firstLongNumericID(value) {
        var match = String(value || '').match(/\d{12,}/);
        return match ? match[0] : '';
    }

    function tokenPayloadUserID(payload) {
        if (!payload || typeof payload !== 'object') return '';
        var keys = ['userId', 'user_id', 'uid', 'id', 'sub'];
        for (var i = 0; i < keys.length; i += 1) {
            var userID = numericUserID(payload[keys[i]]);
            if (userID) return userID;
        }
        return '';
    }

    function tokenUserID() {
        var queryUserID =
            numericUserID(readQuery('userId')) ||
            numericUserID(readQuery('user_id')) ||
            numericUserID(readQuery('uid'));
        if (queryUserID) return queryUserID;

        var token = getAccessToken();
        if (!token) return '';

        var parts = String(token).split('.');
        for (var i = 0; i < parts.length; i += 1) {
            var payloadUserID = tokenPayloadUserID(
                decodeBase64URLJSON(parts[i])
            );
            if (payloadUserID) return payloadUserID;

            var textUserID = firstLongNumericID(decodeBase64URLText(parts[i]));
            if (textUserID) return textUserID;
        }
        return '';
    }

    function isExpiredToken(token) {
        var parts = String(token || '').split('.');
        if (parts.length < 2) return false;
        var payload = decodeBase64URLJSON(parts[1]);
        if (!payload || !payload.exp) return false;
        // JWT exp 是秒级时间戳；本地 H5 不能只凭 query 有 token 就进入 token 模式，过期 token 会被 gateway 当成未登录。
        // 这里预留 5 秒偏移，避免页面发请求时刚好跨过过期点，导致 H5 隐藏账号输入但服务端收到空 display_user_id。
        return Number(payload.exp) * 1000 <= Date.now() + 5000;
    }

    function normalizeBaseURL(value) {
        return String(value || '').replace(/\/+$/, '') + '/';
    }

    function isLocalDevOrigin() {
        var host = window.location.hostname;
        return host === '127.0.0.1' || host === 'localhost' || host === '::1';
    }

    function shouldPersist() {
        return !isLocalDevOrigin();
    }

    function clearLocalDevCache() {
        if (shouldPersist()) return;
        window.localStorage.removeItem(API_ENV_KEY);
        window.localStorage.removeItem(ACCESS_TOKEN_KEY);
        window.localStorage.removeItem(APP_CODE_KEY);
    }

    function devProxyBase(path) {
        return normalizeBaseURL(window.location.origin + path);
    }

    function localDevProxyBase(env) {
        if (!isLocalDevOrigin()) return '';
        if (env === 'test') return devProxyBase('/__api_test__/');
        if (env === 'local') return devProxyBase('/__api_local__/');
        // 本地 H5 调试默认仍访问生产数据，但浏览器只能请求同源代理；由 scripts/h5_dev_server.py 代发到生产 API。
        return devProxyBase('/__api_prod__/');
    }

    function resolveEnv() {
        var env = readQuery('env');
        if (env === 'test' || env === 'local') {
            memoryEnv = env;
            if (shouldPersist()) window.localStorage.setItem(API_ENV_KEY, env);
            return env;
        }
        if (memoryEnv) return memoryEnv;
        if (!shouldPersist()) return '';
        return window.localStorage.getItem(API_ENV_KEY) || '';
    }

    function resolveBaseURL() {
        var env = resolveEnv();
        var configuredAPI = activeAppAPIBaseURL(env);
        if (configuredAPI) return configuredAPI;
        var devProxy = localDevProxyBase(env);
        if (devProxy) return devProxy;
        if (env === 'test') return normalizeBaseURL(test_api);
        if (env === 'local') return normalizeBaseURL(local_api);
        return normalizeBaseURL(default_api);
    }

    function activeAppAPIBaseURL(env) {
        var api = activeAppConfig && activeAppConfig.api;
        if (!api) return '';
        if (env === 'local' && api.local) return normalizeBaseURL(api.local);
        if (env === 'test' && api.test) return normalizeBaseURL(api.test);
        return api.prod || api.default
            ? normalizeBaseURL(api.prod || api.default)
            : '';
    }

    function buildURL(path, query) {
        var url = new URL(path.replace(/^\/+/, ''), resolveBaseURL());
        if (query) {
            Object.keys(query).forEach(function (key) {
                var value = query[key];
                if (value === undefined || value === null || value === '')
                    return;
                url.searchParams.set(key, value);
            });
        }
        return url.toString();
    }

    function isSameOriginURL(url) {
        try {
            return (
                new URL(url, window.location.href).origin ===
                window.location.origin
            );
        } catch (_) {
            return false;
        }
    }

    function getAccessToken() {
        if (memoryAccessToken && !isExpiredToken(memoryAccessToken))
            return memoryAccessToken;
        if (memoryAccessToken && isExpiredToken(memoryAccessToken)) {
            memoryAccessToken = '';
            if (shouldPersist())
                window.localStorage.removeItem(ACCESS_TOKEN_KEY);
        }
        var queryToken =
            normalizeToken(readQuery('token')) ||
            normalizeToken(readQuery('access_token')) ||
            normalizeToken(readQuery('accessToken'));
        if (queryToken) {
            if (isExpiredToken(queryToken)) {
                if (shouldPersist())
                    window.localStorage.removeItem(ACCESS_TOKEN_KEY);
                return '';
            }
            memoryAccessToken = queryToken;
            if (shouldPersist())
                window.localStorage.setItem(ACCESS_TOKEN_KEY, queryToken);
            return queryToken;
        }
        if (!shouldPersist()) return '';
        var storedToken = normalizeToken(
            window.localStorage.getItem(ACCESS_TOKEN_KEY) || ''
        );
        if (storedToken && isExpiredToken(storedToken)) {
            window.localStorage.removeItem(ACCESS_TOKEN_KEY);
            return '';
        }
        return storedToken;
    }

    function themeAppCode(appCode) {
        appCode = String(appCode || 'lalu')
            .trim()
            .toLowerCase();
        // App 身份统一保留在公共主题根节点；未定义专属变量的 Fami/Huwaa 会自然继承 :root 浅紫主题，
        // 后续增加品牌覆盖时无需再改 API/环境解析逻辑。未知 app 仍回退 Lalu，保护旧页面。
        return ['lalu', 'fami', 'huwaa', 'yumi', 'aslan'].indexOf(appCode) >= 0
            ? appCode
            : 'lalu';
    }

    function pageDefaultAppCode() {
        // 新的多产品 H5 入口会在 common/api.js 执行前声明页面所属 App。页面级默认值只在 URL
        // 未显式携带 app_code 时生效，既避免 Fami 页面因客户端漏参串到 Lalu，也不改变历史页面
        // “无参数即 Lalu”的兼容约定。该值不从 localStorage 读取，防止前一次访问污染下一产品。
        return String(window.HyAppDefaultAppCode || '')
            .trim()
            .toLowerCase();
    }

    function requestAppCode() {
        var queryAppCode = String(readQuery('app_code') || '')
            .trim()
            .toLowerCase();
        // X-App-Code 只表达本次 URL 显式指定的后端租户，不能复用 getAppCode 的内存或 localStorage 结果。
        // 这样没有 app_code 参数的入口会稳定落到 lalu，避免历史主题缓存把后续普通页面继续打到 yumi/aslan。
        return queryAppCode || pageDefaultAppCode() || 'lalu';
    }

    function applyAppTheme(appCode) {
        if (!window.document || !window.document.documentElement) return;
        window.document.documentElement.setAttribute(
            'data-hy-app-code',
            themeAppCode(appCode)
        );
    }

    function peekAppCode() {
        var queryAppCode =
            readQuery('app') || readQuery('app_code') || readQuery('appCode');
        if (queryAppCode) return queryAppCode;
        if (pageDefaultAppCode()) return pageDefaultAppCode();
        if (memoryAppCode) return memoryAppCode;
        if (!shouldPersist()) return 'lalu';
        return window.localStorage.getItem(APP_CODE_KEY) || 'lalu';
    }

    function getAppCode() {
        var queryAppCode =
            readQuery('app') || readQuery('app_code') || readQuery('appCode');
        if (queryAppCode) {
            memoryAppCode = queryAppCode;
            if (shouldPersist())
                window.localStorage.setItem(APP_CODE_KEY, queryAppCode);
            applyAppTheme(queryAppCode);
            return queryAppCode;
        }
        if (pageDefaultAppCode()) {
            memoryAppCode = pageDefaultAppCode();
            applyAppTheme(memoryAppCode);
            return memoryAppCode;
        }
        if (memoryAppCode) {
            applyAppTheme(memoryAppCode);
            return memoryAppCode;
        }
        if (!shouldPersist()) {
            applyAppTheme('lalu');
            return 'lalu';
        }
        var storedAppCode = window.localStorage.getItem(APP_CODE_KEY) || 'lalu';
        applyAppTheme(storedAppCode);
        return storedAppCode;
    }

    function setAccessToken(token) {
        token = normalizeToken(token);
        if (token && isExpiredToken(token)) token = '';
        memoryAccessToken = token || '';
        if (token) {
            if (shouldPersist())
                window.localStorage.setItem(ACCESS_TOKEN_KEY, token);
            return;
        }
        if (shouldPersist()) window.localStorage.removeItem(ACCESS_TOKEN_KEY);
    }

    function setAppCode(appCode) {
        memoryAppCode = appCode || '';
        applyAppTheme(appCode || 'lalu');
        if (appCode) {
            if (shouldPersist())
                window.localStorage.setItem(APP_CODE_KEY, appCode);
            return;
        }
        if (shouldPersist()) window.localStorage.removeItem(APP_CODE_KEY);
    }

    applyAppTheme(peekAppCode());

    function setAppConfig(config) {
        activeAppConfig = config || null;
        if (activeAppConfig && activeAppConfig.app) {
            setAppCode(activeAppConfig.app);
        }
        return activeAppConfig;
    }

    function getAppConfig() {
        return activeAppConfig;
    }

    function loadAppConfig(scope) {
        var appCode = String(getAppCode() || 'lalu')
            .trim()
            .toLowerCase();
        if (!appCode || appCode === 'lalu') {
            activeAppConfig = null;
            return Promise.resolve(null);
        }
        var scopePath = String(scope || '').replace(/^\/+|\/+$/g, '');
        var configPath =
            (scopePath ? '/' + scopePath : '.') +
            '/config/' +
            appCode +
            '.json';
        var configURL = new URL(
            configPath,
            window.location.origin + window.location.pathname
        );
        return window
            .fetch(configURL.toString(), {
                cache: shouldPersist() ? 'default' : 'no-store',
            })
            .then(function (response) {
                if (!response.ok) return null;
                return response.json();
            })
            .then(function (config) {
                return setAppConfig(config);
            })
            .catch(function () {
                activeAppConfig = null;
                return null;
            });
    }

    function errorMessageForResponse(response, payload) {
        if (response.status === 409) return 'Duplicate request.';
        return payload.message || response.statusText || 'request_failed';
    }

    function notifyHTTPError(response, message) {
        if (response.status !== 409 || !message) return;
        if (window.HyAppToast && window.HyAppToast.show) {
            window.HyAppToast.show(message);
            return;
        }
        window.dispatchEvent(
            new CustomEvent('hyapp:toast', {
                detail: { message: message },
            })
        );
    }

    function parseResponse(response) {
        return response.text().then(function (text) {
            var payload = text ? JSON.parse(text) : {};
            if (!response.ok) {
                var message = errorMessageForResponse(response, payload);
                notifyHTTPError(response, message);
                var httpError = new Error(message);
                httpError.status = response.status;
                httpError.code = payload.code;
                httpError.request_id = payload.request_id;
                httpError.payload = payload;
                throw httpError;
            }
            var code = payload && payload.code;
            var errorCode = payload && payload.errorCode;
            var isErrorCode =
                (typeof code === 'number' && code !== 0) ||
                (typeof code === 'string' && code !== '' && code !== 'OK') ||
                (typeof errorCode === 'number' && errorCode !== 0) ||
                (typeof errorCode === 'string' &&
                    errorCode !== '' &&
                    errorCode !== '0' &&
                    errorCode !== 'OK');
            if (isErrorCode) {
                var apiError = new Error(
                    payload.message ||
                        payload.errorMsg ||
                        payload.error ||
                        'api_request_failed'
                );
                apiError.code = payload.code || payload.errorCode;
                apiError.request_id = payload.request_id;
                apiError.payload = payload;
                throw apiError;
            }
            return payload &&
                Object.prototype.hasOwnProperty.call(payload, 'data')
                ? payload.data
                : payload &&
                    Object.prototype.hasOwnProperty.call(payload, 'body')
                  ? payload.body
                  : payload;
        });
    }

    function persistRenewedAccessToken(response) {
        var renewedToken =
            response && response.headers
                ? response.headers.get(RENEWED_ACCESS_TOKEN_HEADER)
                : '';
        if (renewedToken) setAccessToken(renewedToken);
    }

    function request(path, options) {
        var opts = options || {};
        var headers = Object.assign(
            {},
            (activeAppConfig && activeAppConfig.headers) || {},
            opts.headers || {}
        );
        var token = getAccessToken();
        var url = buildURL(path, opts.query);
        if (token && !opts.skipAuth) headers.Authorization = 'Bearer ' + token;
        headers['X-App-Code'] = requestAppCode();
        if (!shouldPersist() && isSameOriginURL(url))
            headers['Cache-Control'] = 'no-cache';

        var body = opts.body;
        if (body && typeof body === 'object' && !(body instanceof FormData)) {
            headers['Content-Type'] =
                headers['Content-Type'] || 'application/json';
            body = JSON.stringify(body);
        }

        return window
            .fetch(url, {
                method: opts.method || 'GET',
                headers: headers,
                body: body,
                cache: shouldPersist() ? 'default' : 'no-store',
                credentials: opts.credentials || 'omit',
            })
            .then(function (response) {
                persistRenewedAccessToken(response);
                return parseResponse(response);
            });
    }

    var vipAPI = {
        me: function () {
            return request('/api/v1/vip/me', { method: 'GET' });
        },
        packages: function () {
            return request('/api/v1/vip/packages', { method: 'GET' });
        },
        purchase: function (payload) {
            return request('/api/v1/vip/purchase', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    var userAPI = {
        me: function () {
            return request('/api/v1/users/me', { method: 'GET' });
        },
        overview: function () {
            return request('/api/v1/users/me/overview', { method: 'GET' });
        },
        profile: function (userID, options) {
            var id = String(userID || '').trim();
            var opts = options || {};
            if (!id) return Promise.reject(new Error('user_id is required'));
            return request(
                '/api/v1/users/by-id/' + encodeURIComponent(id) + '/profile',
                {
                    method: 'GET',
                    skipAuth: !!opts.skipAuth,
                }
            );
        },
        appearance: function (userID, options) {
            var id = String(userID || '').trim();
            var opts = options || {};
            return request(
                id
                    ? '/api/v1/users/by-id/' +
                          encodeURIComponent(id) +
                          '/appearance'
                    : '/api/v1/users/me/appearance',
                { method: 'GET', skipAuth: !!opts.skipAuth }
            );
        },
        hostIdentity: function () {
            return request('/api/v1/users/me/host-identity', {
                method: 'GET',
            });
        },
        roleSummary: function () {
            return request('/api/v1/users/me/role-summary', {
                method: 'GET',
            });
        },
        updateContact: function (payload) {
            return request('/api/v1/users/me/contact-info', {
                method: 'POST',
                body: payload || {},
            });
        },
        completeOnboarding: function (payload) {
            return request('/api/v1/users/me/onboarding/complete', {
                method: 'POST',
                body: payload || {},
            });
        },
        resolveDisplayUserID: function (displayUserID) {
            return request(
                '/api/v1/users/by-display-user-id/' +
                    encodeURIComponent(displayUserID || ''),
                {
                    method: 'GET',
                }
            );
        },
        getInviteReferrer: function () {
            return request('/api/v1/users/me/invite-referrer', {
                method: 'GET',
            });
        },
        searchInviteReferrer: function (inviteCode) {
            return request('/api/v1/users/me/invite-referrer/search', {
                method: 'GET',
                query: { invite_code: inviteCode || '' },
            });
        },
        bindInviteReferrer: function (inviteCode) {
            return request('/api/v1/users/me/invite-referrer/bind', {
                method: 'POST',
                body: { invite_code: String(inviteCode || '') },
            });
        },
    };

    var walletAPI = {
        balances: function (assetTypes) {
            var value = Array.isArray(assetTypes)
                ? assetTypes.join(',')
                : assetTypes;
            return request('/api/v1/wallet/me/balances', {
                method: 'GET',
                query: { asset_type: value },
            });
        },
        transactions: function (params) {
            return request('/api/v1/wallet/transactions', {
                method: 'GET',
                query: params || {},
            });
        },
        transferCoinFromSeller: function (payload) {
            return request('/api/v1/wallet/coin-seller/transfer', {
                method: 'POST',
                body: payload || {},
            });
        },
        getCoinSellerSubSellerPermission: function () {
            return request(
                '/api/v1/wallet/coin-seller/sub-sellers/permission',
                {
                    method: 'GET',
                }
            );
        },
        inviteCoinSellerSubSeller: function (payload) {
            return request('/api/v1/wallet/coin-seller/sub-sellers', {
                method: 'POST',
                body: payload || {},
            });
        },
        listCoinSellerSubSellers: function (params) {
            return request('/api/v1/wallet/coin-seller/sub-sellers', {
                method: 'GET',
                query: params || {},
            });
        },
        transferCoinSellerToSubSeller: function (payload) {
            return request('/api/v1/wallet/coin-seller/sub-sellers/transfer', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    // POINT 通用钱包的汇率、手续费和币商白名单全部由服务端快照返回；页面不持有资金参数默认值。
    var pointWalletAPI = {
        overview: function () {
            return request('/api/v1/point-wallet/overview', {
                method: 'GET',
            });
        },
        withdrawUSDT: function (payload) {
            return request('/api/v1/point-wallet/withdraw', {
                method: 'POST',
                body: payload || {},
            });
        },
        transferToCoinSeller: function (payload) {
            return request('/api/v1/point-wallet/transfer-to-coin-seller', {
                method: 'POST',
                body: payload || {},
            });
        },
        history: function (pageSize) {
            return walletAPI.transactions({
                asset_type: 'POINT',
                page: 1,
                page_size: pageSize || 30,
            });
        },
    };

    // H5 充值 API 只做路径和参数名封装；token、app_code、env、错误 envelope 仍由 request 统一处理。
    // 金额、金币数、普通/币商钱包、三方支付参数和 USDT 链上校验都以后端订单快照为准。
    function isAslanWebPay() {
        return activeAppConfig && activeAppConfig.adapter === 'aslan-web-pay';
    }

    function isYumiWebPay() {
        return activeAppConfig && activeAppConfig.adapter === 'yumi-web-pay';
    }

    function isConfiguredWebPay() {
        return isAslanWebPay() || isYumiWebPay();
    }

    function appEndpoint(name, fallback) {
        return (
            (activeAppConfig &&
                activeAppConfig.endpoints &&
                activeAppConfig.endpoints[name]) ||
            fallback
        );
    }

    function normalizeCommodityType(value) {
        var type = String(value || '')
            .trim()
            .toUpperCase();
        return type === 'FREIGHT_GOLD' || type === 'FREIGHT_GOLD_SUPER'
            ? type
            : 'GOLD';
    }

    function configuredCommodityType(extra) {
        return normalizeCommodityType(
            (extra &&
                (extra.commodity_type || extra.commodityType || extra.type)) ||
                (activeAppConfig && activeAppConfig.type) ||
                'GOLD'
        );
    }

    function aslanUserQuery(displayUserID, extra) {
        var value = String(displayUserID || '').trim();
        var query = {
            applicationId: activeAppConfig.application_id,
            type: configuredCommodityType(extra),
            sysOrigin: activeAppConfig.sys_origin || 'ATYOU',
        };
        if (value) {
            query.account = value;
        }
        if (/^\d+$/.test(value)) {
            query.userId = value;
        }
        return query;
    }

    function configuredWebPayUserQuery(displayUserID, extra) {
        var query = Object.assign(aslanUserQuery(displayUserID, extra), {
            sysOrigin: activeAppConfig.sys_origin || 'LIKEI',
        });
        if (isYumiWebPay() && !query.userId && !query.account) {
            // Yumi 的 user-profile 当前只从 query 里的 userId 或 account+sysOrigin 查用户，不会从 Authorization token 反解用户。
            // H5 token 入口会隐藏账号输入并传空 displayUserID，因此这里仅在没有显式身份参数时把 token 里的 uid 补成 userId。
            query.userId = tokenUserID();
        }
        return query;
    }

    function legacyProviderPaymentMethods(options) {
        var providers = (activeAppConfig && activeAppConfig.providers) || {};
        var countryCode = String(
            options.country_code || options.countryCode || ''
        ).toUpperCase();
        var matchedMethods = [];
        var globalMethods = [];
        var allProviderMethods = [];

        // Aslan/Yumi 的 app JSON 是旧支付网关的前端控制面；这里必须逐 provider 收集，
        // 否则新增 V5Pay 后仍只读取 providers.mifapay，会出现配置存在但页面永远不展示的假开通。
        Object.keys(providers).forEach(function (providerKey) {
            var provider = providers[providerKey] || {};
            var countries = Array.isArray(provider.countries)
                ? provider.countries
                : [];
            var matchedCountries = countries.filter(function (country) {
                return countryMatchesCode(country, countryCode);
            });
            var globalCountries = countries.filter(function (country) {
                return countryMatchesCode(country, 'GLOBAL');
            });
            matchedMethods = matchedMethods.concat(
                collectLegacyProviderMethods(
                    matchedCountries,
                    provider,
                    providerKey,
                    options,
                    false
                )
            );
            globalMethods = globalMethods.concat(
                collectLegacyProviderMethods(
                    globalCountries,
                    provider,
                    providerKey,
                    options,
                    false
                )
            );
            allProviderMethods = allProviderMethods.concat(
                collectLegacyProviderMethods(
                    countries,
                    provider,
                    providerKey,
                    options,
                    true
                )
            );
        });

        var methods = matchedMethods.concat(globalMethods);
        if (!matchedMethods.length && countryCode) {
            // 用户国家在 app JSON 里没有具体支付方式时，商品仍按用户国家查询，支付方式降级展示 JSON 中全部方式。
            // 兜底方式只影响前端展示和用户选择，下单仍带当前用户国家的 payCountryId 供后端校验金额和币种。
            methods = allProviderMethods;
        }
        return methods;
    }

    function collectLegacyProviderMethods(
        countries,
        provider,
        providerKey,
        options,
        fallbackAll
    ) {
        var methods = [];
        var providerCode = provider.provider_code || providerKey;
        var providerName = provider.provider_name || providerCode;
        (countries || []).forEach(function (country) {
            var countryCode = primaryCountryCode(country);
            (country.methods || []).forEach(function (method, index) {
                methods.push(
                    Object.assign({}, method, {
                        // method_id 只用于同页稳定区分卡片，不能复制 Lalu 数据库 method_id；
                        // 旧网关真正下单依赖 provider/payWay/payType/channelCode，而不是该前端合成值。
                        method_id: [
                            providerCode,
                            countryCode,
                            method.channel_code || method.pay_way,
                            method.pay_type || method.channel_code,
                            index,
                        ].join(':'),
                        provider_code: providerCode,
                        provider_name: providerName,
                        country_code: countryCode,
                        country_name: country.country_name,
                        currency_code:
                            fallbackAll || countryMatchesCode(country, 'GLOBAL')
                                ? options.currency_code || country.currency_code
                                : country.currency_code,
                        usd_to_currency_rate:
                            options.usd_to_currency_rate ||
                            options.usdToCurrencyRate ||
                            0,
                        pay_country_id:
                            options.pay_country_id ||
                            options.payCountryId ||
                            '',
                        status: provider.status || 'active',
                        fallback_all_countries: !!fallbackAll,
                    })
                );
            });
        });
        return methods;
    }

    function primaryCountryCode(country) {
        country = country || {};
        var codes = countryCodeAliases(country);
        return codes[0] || '';
    }

    function countryMatchesCode(country, code) {
        code = String(code || '')
            .trim()
            .toUpperCase();
        if (!code) return false;
        return countryCodeAliases(country).indexOf(code) >= 0;
    }

    function countryCodeAliases(country) {
        country = country || {};
        var rawCodes = []
            .concat(country.country_code || [])
            .concat(country.countryCode || [])
            .concat(country.country_codes || [])
            .concat(country.countryCodes || []);
        var codes = [];
        rawCodes.forEach(function (value) {
            if (Array.isArray(value)) {
                value.forEach(function (item) {
                    pushCountryCodeAlias(codes, item);
                });
                return;
            }
            pushCountryCodeAlias(codes, value);
        });
        return codes;
    }

    function pushCountryCodeAlias(codes, value) {
        value = String(value || '')
            .trim()
            .toUpperCase();
        if (value && codes.indexOf(value) < 0) {
            codes.push(value);
        }
    }

    function firstValue(object, keys) {
        object = object || {};
        for (var i = 0; i < keys.length; i += 1) {
            var value = object[keys[i]];
            if (value !== undefined && value !== null && value !== '')
                return value;
        }
        return '';
    }

    function normalizeCountryCode(value) {
        return String(value || '')
            .trim()
            .toUpperCase();
    }

    function countryCodeFromCountry(country) {
        country = country || {};
        return normalizeCountryCode(
            firstValue(country, [
                'alphaTwo',
                'alpha_two',
                'countryCode',
                'country_code',
                'code',
            ])
        );
    }

    function countryNameFromCountry(country) {
        return firstValue(country, [
            'aliasName',
            'enName',
            'countryName',
            'country_name',
            'name',
        ]);
    }

    function normalizeFormattedNumber(value) {
        // Yumi legacy commodity responses can serialize large numeric values with
        // thousands separators (for example, "27,600,000"). Normalizing at the
        // adapter boundary preserves the server's numeric value before display
        // and order payload fields independently fall back to zero.
        var amount = Number(String(value || 0).replace(/,/g, ''));
        return Number.isFinite(amount) && amount > 0 ? amount : 0;
    }

    function normalizeAmountUsd(value) {
        return normalizeFormattedNumber(value);
    }

    function decimalToMinor(value) {
        return Math.round(normalizeAmountUsd(value) * 100);
    }

    function booleanValue(value) {
        if (value === true || value === 1) return true;
        var text = String(value || '')
            .trim()
            .toLowerCase();
        return text === 'true' || text === '1' || text === 'yes';
    }

    function mapYumiContext(context) {
        context = context || {};
        var profile = context.userProfile || context.user_profile || {};
        var countryList = context.countryList || context.country_list || [];
        var selectedCountry = countryList[0] || {};
        var country = selectedCountry.country || {};
        var countryCode =
            normalizeCountryCode(profile.countryCode || profile.country_code) ||
            countryCodeFromCountry(country);
        var freightUser = booleanValue(
            profile.freightUser ||
                profile.freight_user ||
                context.freightUser ||
                context.freight_user
        );
        var commodityType = normalizeCommodityType(
            profile.commodityType ||
                profile.commodity_type ||
                context.commodityType ||
                context.commodity_type ||
                (freightUser ? 'FREIGHT_GOLD' : 'GOLD')
        );
        freightUser = freightUser || commodityType === 'FREIGHT_GOLD';
        return {
            auth_mode: 'display_user_id',
            account: {
                id: profile.id || '',
                user_id: profile.id || '',
                userId: profile.id || '',
                display_user_id: profile.account || profile.id || '',
                username: profile.userNickname || profile.user_nickname || '',
                avatar: profile.userAvatar || profile.user_avatar || '',
                country_code: countryCode,
                countryCode: countryCode,
                country_name:
                    profile.countryName ||
                    profile.country_name ||
                    countryNameFromCountry(country),
                countryName:
                    profile.countryName ||
                    profile.country_name ||
                    countryNameFromCountry(country),
                audience_type: freightUser ? 'coin_seller' : 'ordinary_user',
                freight_user: freightUser,
                freightUser: freightUser,
                commodity_type: commodityType,
                commodityType: commodityType,
            },
            country_list: countryList.map(function (item) {
                var itemCountry = item.country || {};
                return {
                    id: item.id || '',
                    pay_country_id:
                        item.payCountryId ||
                        item.pay_country_id ||
                        item.id ||
                        '',
                    payCountryId:
                        item.payCountryId ||
                        item.pay_country_id ||
                        item.id ||
                        '',
                    country_id: item.countryId || item.country_id || '',
                    currency_code: item.currency || item.currency_code || '',
                    currencyCode: item.currency || item.currency_code || '',
                    country_code: countryCodeFromCountry(itemCountry),
                    countryCode: countryCodeFromCountry(itemCountry),
                    country_name: countryNameFromCountry(itemCountry),
                    countryName: countryNameFromCountry(itemCountry),
                    country: itemCountry,
                };
            }),
            commodity_type: commodityType,
            commodityType: commodityType,
            freight_user: freightUser,
            freightUser: freightUser,
            region_id: context.regionId || context.region_id || '',
            regionId: context.regionId || context.region_id || '',
            raw: context,
        };
    }

    function mapYumiOptions(options) {
        options = options || {};
        var products = (options.commodity || options.products || []).map(
            function (item) {
                // Coin quantities use the same formatter because Yumi returns
                // content as a comma-formatted string while the UI expects a number.
                var content = normalizeFormattedNumber(item.content);
                var awardContent = normalizeFormattedNumber(
                    item.awardContent || item.award_content || 0
                );
                var amountUsd = normalizeAmountUsd(
                    item.amountUsd || item.amount_usd
                );
                var localAmount = normalizeAmountUsd(
                    item.amount || item.amountLocal || item.amount_local
                );
                var currencyCode = item.currency || item.currency_code || '';
                return {
                    product_id: item.id,
                    productId: item.id,
                    pay_country_id:
                        item.payCountryId ||
                        item.pay_country_id ||
                        options.payCountryId ||
                        options.pay_country_id ||
                        '',
                    coin_amount:
                        (Number.isFinite(content) ? content : 0) +
                        (Number.isFinite(awardContent) ? awardContent : 0),
                    base_coin_amount: Number.isFinite(content) ? content : 0,
                    award_coin_amount: Number.isFinite(awardContent)
                        ? awardContent
                        : 0,
                    commodity_type:
                        item.type ||
                        item.commodityType ||
                        item.commodity_type ||
                        options.type ||
                        '',
                    commodityType:
                        item.type ||
                        item.commodityType ||
                        item.commodity_type ||
                        options.type ||
                        '',
                    amount_usdt: amountUsd,
                    amount_usdt_micro: Math.round(amountUsd * 1000000),
                    amount_minor: decimalToMinor(amountUsd),
                    // Yumi commodity returns both USD amount and local-currency amount; keep both so H5 display does not treat USD as EGP.
                    local_amount: localAmount,
                    localAmount: localAmount,
                    currency_code: currencyCode,
                    currencyCode: currencyCode,
                    raw: item,
                };
            }
        );
        var firstProduct = products[0] || {};
        var countryCode = normalizeCountryCode(
            options.country_code || options.countryCode || ''
        );
        var currencyCode =
            options.currency_code ||
            options.currencyCode ||
            firstProduct.currency_code ||
            '';
        return {
            products: products,
            payment_methods: yumiPaymentMethods(
                Object.assign({}, options, {
                    pay_country_id:
                        options.payCountryId ||
                        options.pay_country_id ||
                        firstProduct.pay_country_id ||
                        '',
                    country_code: countryCode,
                    currency_code: currencyCode,
                    usd_to_currency_rate:
                        options.usd_to_currency_rate ||
                        options.usdToCurrencyRate ||
                        0,
                })
            ),
            pay_country_id:
                options.payCountryId ||
                options.pay_country_id ||
                firstProduct.pay_country_id ||
                '',
            country_code: countryCode,
            currency_code: currencyCode,
            usd_to_currency_rate:
                options.usd_to_currency_rate || options.usdToCurrencyRate || 0,
            usdt_trc20_enabled: false,
            raw: options,
        };
    }

    function yumiPaymentMethods(options) {
        var channels =
            (options.raw && options.raw.channels) || options.channels || [];
        var supported = {};
        channels.forEach(function (item) {
            var channel = item.channel || {};
            var details = item.details || {};
            [
                channel.channelCode,
                channel.channel_code,
                details.channelCode,
                details.channel_code,
                details.factoryChannel,
                details.factory_channel,
            ].forEach(function (code) {
                if (code) supported[String(code).toUpperCase()] = item;
            });
        });
        return legacyProviderPaymentMethods(options).filter(function (method) {
            if (method.provider_code === 'v5pay') {
                // Yumi 的旧 options 接口只返回数据库渠道明细；V5Pay 由服务端在下单时根据
                // provider/payType 合成受控快照，因此不能拿 MiFaPay 的旧渠道集合误删 V5Pay 入口。
                return true;
            }
            var channelCode =
                method.channel_code ||
                [method.pay_way, method.pay_type].filter(Boolean).join('_');
            if (!Object.keys(supported).length) return true;
            return !!supported[String(channelCode || '').toUpperCase()];
        });
    }

    function providerCodeFromFactory(factoryCode) {
        // Java 旧网关返回 factory code，而 H5 状态机统一消费小写 provider code；
        // 显式映射能避免 V5_PAY 被当成未知的重定向渠道并以大写值持久化。
        if (factoryCode === 'MIFA_PAY' || factoryCode === 'MIFAPAY')
            return 'mifapay';
        if (factoryCode === 'V5_PAY' || factoryCode === 'V5PAY') return 'v5pay';
        return factoryCode;
    }

    function mapYumiOrder(order) {
        order = order || {};
        var factoryCode = String(
            order.factoryCode ||
                order.factory_code ||
                order.providerCode ||
                order.provider_code ||
                ''
        ).toUpperCase();
        return {
            order_id: order.orderId || order.order_id || '',
            trade_no: order.tradeNo || order.trade_no || '',
            provider_code: providerCodeFromFactory(factoryCode),
            factory_code: factoryCode,
            pay_url:
                order.requestUrl || order.request_url || order.pay_url || '',
            currency_code: order.currency || order.currency_code || '',
            country_code: order.countryCode || order.country_code || '',
            provider_amount_minor: decimalToMinor(order.amount),
            status: 'pending',
            raw: order,
        };
    }

    function mapYumiOrderStatus(order) {
        order = order || {};
        var status = String(order.status || '').toUpperCase();
        var normalized =
            order.success === true || status === 'SUCCESS'
                ? 'credited'
                : order.finished === true || status === 'FAIL'
                  ? 'failed'
                  : 'pending';
        var factoryCode = String(
            order.factoryCode ||
                order.factory_code ||
                order.providerCode ||
                order.provider_code ||
                ''
        ).toUpperCase();
        return {
            order_id: order.orderId || order.order_id || '',
            trade_no: order.tradeNo || order.trade_no || '',
            provider_code: providerCodeFromFactory(factoryCode),
            factory_code: factoryCode,
            currency_code: order.currency || order.currency_code || '',
            provider_amount_minor: decimalToMinor(order.amount),
            status: normalized,
            failure_reason: order.reason || '',
            raw: order,
        };
    }

    var rechargeAPI = {
        context: function (displayUserID) {
            if (isAslanWebPay()) {
                return request(
                    appEndpoint('context', '/order/web/pay/h5/context'),
                    {
                        method: 'GET',
                        query: aslanUserQuery(displayUserID),
                    }
                );
            }
            if (isYumiWebPay()) {
                return request(
                    appEndpoint('context', '/order/web/pay/user-profile'),
                    {
                        method: 'GET',
                        query: configuredWebPayUserQuery(displayUserID),
                    }
                ).then(mapYumiContext);
            }
            return request('/api/v1/recharge/h5/context', {
                method: 'GET',
                query: { display_user_id: displayUserID || '' },
            });
        },
        options: function (displayUserID, extra) {
            if (isConfiguredWebPay()) {
                var query = Object.assign(
                    aslanUserQuery(displayUserID, extra),
                    {
                        payCountryId:
                            extra &&
                            (extra.pay_country_id || extra.payCountryId),
                        countryCode:
                            extra && (extra.country_code || extra.countryCode),
                        regionId: extra && (extra.region_id || extra.regionId),
                        userId: extra && (extra.user_id || extra.userId),
                    }
                );
                return request(
                    appEndpoint('options', '/order/web/pay/h5/options'),
                    {
                        method: 'GET',
                        query: query,
                    }
                ).then(function (options) {
                    options = options || {};
                    if (isYumiWebPay()) {
                        return mapYumiOptions(
                            Object.assign({}, options, {
                                pay_country_id:
                                    query.payCountryId ||
                                    options.pay_country_id ||
                                    options.payCountryId ||
                                    '',
                                country_code:
                                    query.countryCode || options.country_code,
                            })
                        );
                    }
                    options.payment_methods =
                        legacyProviderPaymentMethods(options);
                    options.usdt_trc20_enabled = false;
                    return options;
                });
            }
            return request('/api/v1/recharge/h5/options', {
                method: 'GET',
                query: { display_user_id: displayUserID || '' },
            });
        },
        createOrder: function (payload) {
            if (isAslanWebPay()) {
                return request(
                    appEndpoint('orders', '/order/web/pay/h5/orders'),
                    {
                        method: 'POST',
                        body: {
                            applicationId: activeAppConfig.application_id,
                            goodsId: payload && payload.product_id,
                            payCountryId: payload && payload.pay_country_id,
                            userId: payload && payload.user_id,
                            providerCode: payload && payload.provider_code,
                            payWay: payload && payload.pay_way,
                            payType: payload && payload.pay_type,
                            channelCode: payload && payload.channel_code,
                            returnUrl: payload && payload.return_url,
                            commandId: payload && payload.command_id,
                            type: configuredCommodityType(payload),
                            language: readLanguageQuery() || 'en',
                        },
                    }
                );
            }
            if (isYumiWebPay()) {
                return request(
                    appEndpoint('orders', '/order/web/pay/recharge'),
                    {
                        method: 'POST',
                        body: {
                            applicationId: activeAppConfig.application_id,
                            goodsId: payload && payload.product_id,
                            payCountryId: payload && payload.pay_country_id,
                            userId: payload && payload.user_id,
                            // Yumi 旧接口以前只收到 channelCode，无法区分同一渠道下的 MiFaPay/V5Pay。
                            // provider 与 V5Pay productType/productMode 必须原样透传，让后端策略选择和签名字段都以用户选择为准。
                            providerCode: payload && payload.provider_code,
                            payWay: payload && payload.pay_way,
                            payType: payload && payload.pay_type,
                            channelCode: payload && payload.channel_code,
                            countryCode: payload && payload.country_code,
                            currencyCode: payload && payload.currency_code,
                            returnUrl: payload && payload.return_url,
                            commandId: payload && payload.command_id,
                            language: readLanguageQuery() || 'en',
                            newVersion: true,
                            appVersion: 'h5',
                        },
                    }
                ).then(mapYumiOrder);
            }
            return request('/api/v1/recharge/h5/orders', {
                method: 'POST',
                query: { language: readLanguageQuery() },
                body: payload || {},
            });
        },
        submitTx: function (orderID, payload) {
            return request(
                '/api/v1/recharge/h5/orders/' +
                    encodeURIComponent(orderID || '') +
                    '/tx',
                {
                    method: 'POST',
                    body: payload || {},
                }
            );
        },
        getOrder: function (orderID, displayUserID) {
            if (isAslanWebPay()) {
                return request(
                    appEndpoint(
                        'order',
                        '/order/web/pay/h5/orders/{orderId}'
                    ).replace('{orderId}', encodeURIComponent(orderID || '')),
                    { method: 'GET' }
                );
            }
            if (isYumiWebPay()) {
                return request(
                    appEndpoint('order', '/order/web/pay/order-status'),
                    {
                        method: 'GET',
                        query: {
                            orderId: orderID || '',
                            userId: displayUserID || '',
                        },
                    }
                ).then(mapYumiOrderStatus);
            }
            return request(
                '/api/v1/recharge/h5/orders/' +
                    encodeURIComponent(orderID || ''),
                {
                    method: 'GET',
                    query: { display_user_id: displayUserID || '' },
                }
            );
        },
    };

    var salaryWalletAPI = {
        overview: function (identity) {
            return request('/api/v1/salary-wallet/overview', {
                method: 'GET',
                query: { identity: identity },
            });
        },
        history: function (identity, limit) {
            return request('/api/v1/salary-wallet/history', {
                method: 'GET',
                query: {
                    identity: identity,
                    page_size: limit || 30,
                },
            });
        },
        searchCoinSeller: function (displayUserID, identity) {
            return request('/api/v1/salary-wallet/coin-sellers/search', {
                method: 'GET',
                query: {
                    display_user_id: displayUserID,
                    identity: identity,
                },
            });
        },
        exchange: function (payload) {
            return request('/api/v1/salary-wallet/exchange', {
                method: 'POST',
                body: payload || {},
            });
        },
        transferToCoinSeller: function (payload) {
            return request('/api/v1/salary-wallet/transfer-to-coin-seller', {
                method: 'POST',
                body: payload || {},
            });
        },
        getWithdrawAddress: function () {
            return request('/api/v1/salary-wallet/withdraw-address', {
                method: 'GET',
            });
        },
        saveWithdrawAddress: function (payload) {
            return request('/api/v1/salary-wallet/withdraw-address', {
                method: 'POST',
                body: payload || {},
            });
        },
        withdraw: function (payload) {
            return request('/api/v1/salary-wallet/withdraw', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    var hostAPI = {
        searchAgencies: function (shortId, pageSize) {
            return request('/api/v1/host/agencies/search', {
                method: 'GET',
                query: { short_id: shortId, page_size: pageSize || 20 },
            });
        },
        applyToAgency: function (payload) {
            return request('/api/v1/host/agency-applications', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    var agencyCenterAPI = {
        overview: function () {
            return request('/api/v1/agency-center/overview', {
                method: 'GET',
            });
        },
        platformPolicy: function () {
            return request('/api/v1/agency-center/platform-policy', {
                method: 'GET',
            });
        },
        hosts: function () {
            return request('/api/v1/agency-center/hosts', {
                method: 'GET',
            });
        },
        stats: function (query) {
            // 公会统计只接受自然日范围；收益、分成和主播在线数据均由 owner service 聚合。
            return request('/api/v1/agency-center/stats', {
                method: 'GET',
                query: query || {},
            });
        },
        applications: function (status) {
            return request('/api/v1/agency-center/applications', {
                method: 'GET',
                query: { status: status || 'pending' },
            });
        },
        reviewApplication: function (applicationId, payload) {
            return request(
                '/api/v1/agency-center/applications/' +
                    encodeURIComponent(applicationId || '') +
                    '/review',
                {
                    method: 'POST',
                    body: payload || {},
                }
            );
        },
        removeHost: function (hostUserId, payload) {
            return request(
                '/api/v1/agency-center/hosts/' +
                    encodeURIComponent(hostUserId || '') +
                    '/remove',
                {
                    method: 'POST',
                    body: payload || {},
                }
            );
        },
        inviteHost: function (payload) {
            return request('/api/v1/agency-center/invitations/host', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    var hostCenterAPI = {
        agency: function () {
            return request('/api/v1/host-center/agency', {
                method: 'GET',
            });
        },
        stats: function (query) {
            // Fami 新主播中心按任意自然日区间读取服务端聚合；前端只传日期，不上传时长或收益值。
            return request('/api/v1/host-center/stats', {
                method: 'GET',
                query: query || {},
            });
        },
        platformPolicy: function () {
            return request('/api/v1/host-center/platform-policy', {
                method: 'GET',
            });
        },
    };

    var bdLeaderCenterAPI = {
        overview: function () {
            return request('/api/v1/bd-leader-center/overview', {
                method: 'GET',
            });
        },
        bds: function (pageSize) {
            return request('/api/v1/bd-leader-center/bds', {
                method: 'GET',
                query: { page_size: pageSize || 20 },
            });
        },
        agencies: function (pageSize) {
            return request('/api/v1/bd-leader-center/agencies', {
                method: 'GET',
                query: { page_size: pageSize || 20 },
            });
        },
        inviteBD: function (payload) {
            return request('/api/v1/bd-leader/invitations/bd', {
                method: 'POST',
                body: payload || {},
            });
        },
        inviteAgency: function (payload) {
            return request('/api/v1/bd-leader/invitations/agency', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    var bdCenterAPI = {
        overview: function () {
            return request('/api/v1/bd-center/overview', {
                method: 'GET',
            });
        },
        agencies: function (pageSize) {
            return request('/api/v1/bd-center/agencies', {
                method: 'GET',
                query: { page_size: pageSize || 20 },
            });
        },
        stats: function (query) {
            return request('/api/v1/bd-center/stats', {
                method: 'GET',
                query: query || {},
            });
        },
        inviteAgency: function (payload) {
            return request('/api/v1/bd/invitations/agency', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    var roleInvitationAPI = {
        list: function (status, pageSize) {
            return request('/api/v1/role-invitations', {
                method: 'GET',
                query: {
                    status: status || 'pending',
                    page_size: pageSize || 50,
                },
            });
        },
        process: function (invitationID, payload) {
            return request(
                '/api/v1/role-invitations/' +
                    encodeURIComponent(invitationID || '') +
                    '/process',
                {
                    method: 'POST',
                    body: payload || {},
                }
            );
        },
    };

    var managerCenterAPI = {
        overview: function () {
            return request('/api/v1/manager-center/overview', {
                method: 'GET',
            });
        },
        searchUsers: function (keyword, pageSize, scope, action) {
            return request('/api/v1/manager-center/users/search', {
                method: 'GET',
                query: {
                    keyword: keyword,
                    page_size: pageSize || 20,
                    scope: scope,
                    action: action,
                },
            });
        },
        listCountries: function () {
            return request('/api/v1/countries', {
                method: 'GET',
            });
        },
        listResources: function (resourceType, pageSize, page) {
            return request('/api/v1/manager-center/resource-grants/resources', {
                method: 'GET',
                query: {
                    resource_type: resourceType,
                    page: page || 1,
                    page_size: pageSize || 20,
                },
            });
        },
        listAllResources: function (resourceType, pageSize) {
            var requestedSize = Math.floor(Number(pageSize));
            var size =
                Number.isFinite(requestedSize) && requestedSize > 0
                    ? Math.min(requestedSize, 20)
                    : 20;

            // Gateway 单页最多返回 20 条；经理中心必须根据 total 拉完剩余分页，否则较早创建但已开启赠送的资源会永久被截断。
            return managerCenterAPI
                .listResources(resourceType, size, 1)
                .then(function (firstPage) {
                    var firstItems = (firstPage && firstPage.items) || [];
                    var total = Number(firstPage && firstPage.total);
                    if (!Number.isFinite(total) || total < firstItems.length) {
                        total = firstItems.length;
                    }
                    var pageCount = Math.max(1, Math.ceil(total / size));
                    if (pageCount === 1) return firstPage || { items: [] };

                    var remainingRequests = [];
                    for (var page = 2; page <= pageCount; page += 1) {
                        remainingRequests.push(
                            managerCenterAPI.listResources(
                                resourceType,
                                size,
                                page
                            )
                        );
                    }
                    return Promise.all(remainingRequests).then(
                        function (remainingPages) {
                            var resources = [];
                            var seenResourceIDs = Object.create(null);
                            [firstPage]
                                .concat(remainingPages)
                                .forEach(function (pageData) {
                                    (
                                        (pageData && pageData.items) ||
                                        []
                                    ).forEach(function (resource) {
                                        var resourceID =
                                            resource &&
                                            (resource.resource_id ||
                                                resource.resourceId);
                                        // 分页期间后台可能调整排序；按资源 ID 去重可避免同一素材在两个相邻页重复展示。
                                        if (
                                            resourceID &&
                                            seenResourceIDs[String(resourceID)]
                                        ) {
                                            return;
                                        }
                                        if (resourceID) {
                                            seenResourceIDs[
                                                String(resourceID)
                                            ] = true;
                                        }
                                        resources.push(resource);
                                    });
                                });
                            return Object.assign({}, firstPage || {}, {
                                items: resources,
                                page: 1,
                                page_size: size,
                                total: Math.max(total, resources.length),
                            });
                        }
                    );
                });
        },
        grantResource: function (payload) {
            return request('/api/v1/manager-center/resource-grants', {
                method: 'POST',
                body: payload || {},
            });
        },
        grantVip: function (payload) {
            return request('/api/v1/manager-center/vip-grants', {
                method: 'POST',
                body: payload || {},
            });
        },
        listBlocks: function (status, pageSize) {
            return request('/api/v1/manager-center/blocks', {
                method: 'GET',
                query: {
                    status: status || 'active',
                    page_size: pageSize || 50,
                },
            });
        },
        blockUser: function (payload) {
            return request('/api/v1/manager-center/blocks', {
                method: 'POST',
                body: payload || {},
            });
        },
        unblockUser: function (blockId, payload) {
            return request(
                '/api/v1/manager-center/blocks/' +
                    encodeURIComponent(blockId || '') +
                    '/unblock',
                {
                    method: 'POST',
                    body: payload || {},
                }
            );
        },
        updateLevel: function (payload) {
            return request('/api/v1/manager-center/levels', {
                method: 'POST',
                body: payload || {},
            });
        },
        createBDLeader: function (payload) {
            return request('/api/v1/manager-center/bd-leaders', {
                method: 'POST',
                body: payload || {},
            });
        },
        transferUserCountry: function (payload) {
            return request('/api/v1/manager-center/users/country', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    var superadminCenterAPI = bdLeaderCenterAPI;

    var levelAPI = {
        overview: function () {
            return request('/api/v1/levels/me/overview', { method: 'GET' });
        },
        track: function (track) {
            return request(
                '/api/v1/levels/tracks/' + encodeURIComponent(track || ''),
                {
                    method: 'GET',
                }
            );
        },
        rewards: function (query) {
            return request('/api/v1/levels/rewards', {
                method: 'GET',
                query: query || {},
            });
        },
    };

    // 成就页只读当前用户的 App 成就和徽章展示资料；认证、app_code、env、错误 envelope 仍由 request 统一处理。
    // collection_type 由页面按 tab 传入，避免 H5 自己拼接不同业务集合导致后端筛选语义漂移。
    var achievementAPI = {
        list: function (query, options) {
            var opts = options || {};
            return request('/api/v1/achievements', {
                method: 'GET',
                query: query || {},
                skipAuth: !!opts.skipAuth,
            });
        },
        badges: function () {
            return request('/api/v1/badges/me', { method: 'GET' });
        },
        setDisplay: function (payload) {
            return request('/api/v1/badges/display', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    // 任务页 API 只封装 H5 需要的两个 App 合约；登录态、app_code、env 和错误 envelope 仍由 request 统一处理。
    // 领取奖励的 command_id 必须来自点击动作本身，页面脚本负责生成并在同一次重试中复用，避免重复点击造成多次发奖。
    var taskAPI = {
        tabs: function () {
            return request('/api/v1/tasks/tabs', { method: 'GET' });
        },
        claim: function (payload) {
            return request('/api/v1/tasks/claim', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    var weeklyStarAPI = {
        current: function () {
            return request('/api/v1/activities/weekly-star/current', {
                method: 'GET',
            });
        },
        leaderboard: function (params) {
            return request('/api/v1/activities/weekly-star/leaderboard', {
                method: 'GET',
                query: params || {},
            });
        },
        history: function (limit) {
            return request('/api/v1/activities/weekly-star/history', {
                method: 'GET',
                query: { limit: limit || 10 },
            });
        },
    };

    var cpWeeklyRankAPI = {
        status: function (params) {
            return request('/api/v1/activities/cp-weekly-rank', {
                method: 'GET',
                query: params || {},
            });
        },
    };

    var cpSpaceAPI = {
        relationships: function (params) {
            return request('/api/v1/cp/relationships', {
                method: 'GET',
                query: params || {},
            });
        },
        formationGiftFeed: function (params) {
            return request('/api/v1/cp/formation-gift-feed', {
                method: 'GET',
                query: params || {},
            });
        },
        intimacyLeaderboard: function (params) {
            return request('/api/v1/cp/intimacy-leaderboard', {
                method: 'GET',
                query: params || {},
            });
        },
        breakRelationship: function (relationshipID, payload) {
            return request(
                '/api/v1/cp/relationships/' +
                    encodeURIComponent(relationshipID || '') +
                    '/break',
                {
                    method: 'POST',
                    body: payload || {},
                }
            );
        },
    };

    // 转盘 H5 只通过 gateway 活动接口读配置、抽奖、查记录和跑马灯提示；页面不再内置 mock 奖品，避免后台资源组未配置时前端仍展示假奖池。
    // 抽奖 command_id 由页面点击时生成并随请求提交，服务端用它做幂等，网络重试不会重复扣费或重复发奖。
    var activityWheelAPI = {
        config: function (wheelID) {
            return request(
                '/api/v1/activity/wheel/' +
                    encodeURIComponent(wheelID || 'classic') +
                    '/config',
                { method: 'GET' }
            );
        },
        draw: function (wheelID, payload) {
            return request(
                '/api/v1/activity/wheel/' +
                    encodeURIComponent(wheelID || 'classic') +
                    '/draw',
                {
                    method: 'POST',
                    body: payload || {},
                }
            );
        },
        history: function (wheelID, params) {
            return request(
                '/api/v1/activity/wheel/' +
                    encodeURIComponent(wheelID || 'classic') +
                    '/history',
                {
                    method: 'POST',
                    query: params || {},
                    body: {},
                }
            );
        },
        hints: function (wheelID) {
            return request(
                '/api/v1/activity/wheel/' +
                    encodeURIComponent(wheelID || 'classic') +
                    '/hints',
                {
                    method: 'POST',
                    body: {},
                }
            );
        },
    };

    // 邀请活动 H5 只关心当前用户本月状态和领取动作；有效人数、累计充值和最高档差额都以后端聚合事实为准。
    // 领取时页面必须传入本次点击生成的 command_id，服务端按用户、周期、档位和 command_id 做幂等发奖。
    var inviteActivityRewardAPI = {
        status: function () {
            return request('/api/v1/activities/invite-reward', {
                method: 'GET',
            });
        },
        leaderboard: function (params) {
            return request('/api/v1/activities/invite-reward/leaderboard', {
                method: 'GET',
                query: params || {},
            });
        },
        claim: function (payload) {
            return request('/api/v1/activities/invite-reward/claim', {
                method: 'POST',
                body: payload || {},
            });
        },
    };

    var diceGameAPI = {
        config: function (gameID) {
            return request('/api/v1/games/dice/config', {
                method: 'GET',
                query: { game_id: gameID || 'dice' },
            });
        },
        match: function (payload) {
            return request('/api/v1/games/dice/match', {
                method: 'POST',
                body: payload || {},
            });
        },
        getMatch: function (matchID) {
            return request(
                '/api/v1/games/dice/matches/' +
                    encodeURIComponent(matchID || ''),
                { method: 'GET' }
            );
        },
        roll: function (matchID) {
            return request(
                '/api/v1/games/dice/matches/' +
                    encodeURIComponent(matchID || '') +
                    '/roll',
                { method: 'POST', body: {} }
            );
        },
        cancel: function (matchID) {
            return request(
                '/api/v1/games/dice/matches/' +
                    encodeURIComponent(matchID || '') +
                    '/cancel',
                { method: 'POST', body: {} }
            );
        },
    };

    var rpsGameAPI = {
        config: function (gameID) {
            return request('/api/v1/games/rps/config', {
                method: 'GET',
                query: { game_id: gameID || 'rock' },
            });
        },
        match: function (payload) {
            return request('/api/v1/games/rps/match', {
                method: 'POST',
                body: payload || {},
            });
        },
        create: function (payload) {
            return request('/api/v1/games/rps/matches', {
                method: 'POST',
                body: payload || {},
            });
        },
        join: function (matchID, payload) {
            return request(
                '/api/v1/games/rps/matches/' +
                    encodeURIComponent(matchID || '') +
                    '/join',
                { method: 'POST', body: payload || {} }
            );
        },
        getMatch: function (matchID) {
            return request(
                '/api/v1/games/rps/matches/' +
                    encodeURIComponent(matchID || ''),
                { method: 'GET' }
            );
        },
        roll: function (matchID, payload) {
            return request(
                '/api/v1/games/rps/matches/' +
                    encodeURIComponent(matchID || '') +
                    '/roll',
                { method: 'POST', body: payload || {} }
            );
        },
        cancel: function (matchID) {
            return request(
                '/api/v1/games/rps/matches/' +
                    encodeURIComponent(matchID || '') +
                    '/cancel',
                { method: 'POST', body: {} }
            );
        },
    };

    function analyticsEventID(prefix) {
        var random = '';
        if (window.crypto && window.crypto.getRandomValues) {
            var bytes = new Uint32Array(2);
            window.crypto.getRandomValues(bytes);
            random = bytes[0].toString(36) + bytes[1].toString(36);
        } else {
            random = Math.random().toString(36).slice(2);
        }
        return (
            String(prefix || 'self_game') +
            '_' +
            Date.now().toString(36) +
            '_' +
            random
        );
    }

    function reportSelfGameEvent(payload) {
        var body = Object.assign({}, payload || {});
        body.event_id = body.event_id || analyticsEventID(body.event_name);
        body.occurred_at_ms = body.occurred_at_ms || Date.now();
        return request('/api/v1/games/self/report-events', {
            method: 'POST',
            body: body,
        });
    }

    var gameAPI = {
        dice: diceGameAPI,
        rps: rpsGameAPI,
        reportEvent: reportSelfGameEvent,
    };

    window.HyAppAPI = {
        baseURL: resolveBaseURL,
        buildURL: buildURL,
        getAccessToken: getAccessToken,
        getAppCode: getAppCode,
        setAccessToken: setAccessToken,
        setAppCode: setAppCode,
        setAppConfig: setAppConfig,
        getAppConfig: getAppConfig,
        loadAppConfig: loadAppConfig,
        request: request,
        get: function (path, query) {
            return request(path, { method: 'GET', query: query });
        },
        post: function (path, body, query) {
            return request(path, { method: 'POST', body: body, query: query });
        },
        put: function (path, body, query) {
            return request(path, { method: 'PUT', body: body, query: query });
        },
        patch: function (path, body, query) {
            return request(path, { method: 'PATCH', body: body, query: query });
        },
        delete: function (path, query) {
            return request(path, { method: 'DELETE', query: query });
        },
        vip: vipAPI,
        user: userAPI,
        wallet: walletAPI,
        pointWallet: pointWalletAPI,
        recharge: rechargeAPI,
        salaryWallet: salaryWalletAPI,
        host: hostAPI,
        hostCenter: hostCenterAPI,
        agencyCenter: agencyCenterAPI,
        bdCenter: bdCenterAPI,
        bdLeaderCenter: bdLeaderCenterAPI,
        roleInvitations: roleInvitationAPI,
        managerCenter: managerCenterAPI,
        superadminCenter: superadminCenterAPI,
        activityWheel: activityWheelAPI,
        level: levelAPI,
        achievement: achievementAPI,
        task: taskAPI,
        weeklyStar: weeklyStarAPI,
        cpWeeklyRank: cpWeeklyRankAPI,
        cpSpace: cpSpaceAPI,
        inviteActivityReward: inviteActivityRewardAPI,
        game: gameAPI,
    };
    clearLocalDevCache();
})();
