package dashboard

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	"hyapp-admin-server/internal/config"
)

type AslanHTTPDashboardSource struct {
	client          *http.Client
	appCode         string
	appName         string
	baseURL         string
	overviewPath    string
	statTimezone    string
	requestTimeout  time.Duration
	regionResolvers []ExternalDashboardRegionResolver
}

type aslanRegionStatistics struct {
	Date              string                   `json:"date"`
	RegionID          string                   `json:"regionId"`
	RegionCode        string                   `json:"regionCode"`
	RegionName        string                   `json:"regionName"`
	DailyRecharge     aslanDecimal             `json:"dailyRecharge"`
	DailyActive       aslanInt64               `json:"dailyActive"`
	DailyRegister     aslanInt64               `json:"dailyRegister"`
	GoldBalance       aslanInt64               `json:"goldBalance"`
	HostSalaryBalance aslanDecimal             `json:"hostSalaryBalance"`
	Countries         []aslanCountryStatistics `json:"countries"`
}

type aslanCountryStatistics struct {
	CountryCode       string       `json:"countryCode"`
	CountryName       string       `json:"countryName"`
	DailyRecharge     aslanDecimal `json:"dailyRecharge"`
	DailyActive       aslanInt64   `json:"dailyActive"`
	DailyRegister     aslanInt64   `json:"dailyRegister"`
	GoldBalance       aslanInt64   `json:"goldBalance"`
	HostSalaryBalance aslanDecimal `json:"hostSalaryBalance"`
}

type aslanDecimal struct {
	raw string
}

type aslanInt64 struct {
	value int64
}

type aslanDashboardMetric struct {
	RechargeUSDMinor int64
	ActiveUsers      int64
	NewUsers         int64
	PaidUsers        int64
	CoinTotal        int64
	SalaryUSDMinor   int64
}

type aslanCountryAccumulator struct {
	CountryCode string
	CountryName string
	RegionID    int64
	RegionCode  string
	RegionName  string
	Flow        aslanDashboardMetric
	Snapshot    aslanDashboardMetric
}

// NewAslanHTTPDashboardSource 接入 Aslan console 已暴露的免鉴权数据口径；admin 不连接 Aslan 数据库，
// 只把远端接口结果转换成本后台大屏已有的响应字段。
func NewAslanHTTPDashboardSource(sourceConfig config.DashboardExternalSourceConfig, regionResolvers ...ExternalDashboardRegionResolver) (ExternalDashboardSource, error) {
	if !sourceConfig.Enabled {
		return nil, nil
	}
	appCode := strings.ToLower(strings.TrimSpace(sourceConfig.AppCode))
	if appCode == "" {
		return nil, nil
	}
	baseURL := strings.TrimRight(strings.TrimSpace(sourceConfig.BaseURL), "/")
	overviewPath := "/" + strings.TrimLeft(strings.TrimSpace(sourceConfig.OverviewPath), "/")
	if baseURL == "" || overviewPath == "/" {
		return nil, fmt.Errorf("aslan dashboard source %s requires base_url and overview_path", appCode)
	}
	parsed, err := url.Parse(baseURL)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		if err == nil {
			err = fmt.Errorf("missing scheme or host")
		}
		return nil, fmt.Errorf("parse aslan dashboard base_url %q: %w", baseURL, err)
	}
	statTimezone := strings.TrimSpace(sourceConfig.StatTimezone)
	if statTimezone == "" {
		statTimezone = "Asia/Shanghai"
	}
	timeout := sourceConfig.RequestTimeout
	if timeout <= 0 {
		timeout = 5 * time.Second
	}
	return &AslanHTTPDashboardSource{
		client:          &http.Client{Timeout: timeout},
		appCode:         appCode,
		appName:         strings.TrimSpace(sourceConfig.AppName),
		baseURL:         baseURL,
		overviewPath:    overviewPath,
		statTimezone:    statTimezone,
		requestTimeout:  timeout,
		regionResolvers: compactExternalDashboardRegionResolvers(regionResolvers),
	}, nil
}

func (s *AslanHTTPDashboardSource) AppCode() string {
	if s == nil {
		return ""
	}
	return s.appCode
}

func (s *AslanHTTPDashboardSource) StatisticsOverview(ctx context.Context, query StatisticsQuery) (map[string]any, error) {
	if s == nil || s.client == nil {
		return nil, fmt.Errorf("aslan dashboard source is not configured")
	}
	countryFilter, err := s.countryFilter(ctx, query)
	if err != nil {
		return nil, err
	}
	requestTimezone := strings.TrimSpace(query.StatTZ)
	if requestTimezone == "" {
		requestTimezone = s.statTimezone
	}
	requestLocation, err := time.LoadLocation(requestTimezone)
	if err != nil {
		return nil, fmt.Errorf("load aslan dashboard request timezone %s: %w", requestTimezone, err)
	}
	if _, err := time.LoadLocation(s.statTimezone); err != nil {
		return nil, fmt.Errorf("load aslan dashboard storage timezone %s: %w", s.statTimezone, err)
	}
	startDate, endDate, startMS, endMS := externalDashboardDateRange(query, requestLocation)

	queryCtx, cancel := context.WithTimeout(ctx, s.requestTimeout)
	defer cancel()

	current, err := s.loadRange(queryCtx, startDate, endDate, countryFilter)
	if err != nil {
		return nil, err
	}
	days := countCalendarDays(startDate, endDate)
	previousStart := startDate.AddDate(0, 0, -days)
	previous, err := s.loadRange(queryCtx, previousStart, startDate, countryFilter)
	if err != nil {
		return nil, err
	}

	response := current.Total.toMap()
	response["app_code"] = s.appCode
	response["app_name"] = firstExternalDashboardValue(s.appName, s.appCode)
	response["sys_origin"] = "ATYOU"
	response["stat_tz"] = s.statTimezone
	response["request_stat_tz"] = requestTimezone
	response["start_ms"] = startMS
	response["end_ms"] = endMS
	response["daily_series"] = current.DailySeries
	response["country_breakdown"] = current.CountryBreakdown
	response["daily_country_breakdown"] = current.DailyCountryBreakdown
	response["retention"] = aslanRetentionMap(current.Total)
	response["report_metric_sources"] = aslanDashboardMetricSources()
	response["updated_at_ms"] = time.Now().UTC().UnixMilli()
	applyExternalDashboardDeltas(response, current.Total, previous.Total)
	response["coin_total_delta_rate"] = deltaRate(nullableMetricInt64(current.Total.CoinTotal), nullableMetricInt64(previous.Total.CoinTotal))
	response["salary_delta_rate"] = deltaRate(nullableMetricInt64(current.Total.SalaryUSDMinor), nullableMetricInt64(previous.Total.SalaryUSDMinor))
	applyAslanUnavailableFields(response)
	return response, nil
}

type aslanRangeOverview struct {
	Total                 externalDashboardMetric
	DailySeries           []map[string]any
	CountryBreakdown      []map[string]any
	DailyCountryBreakdown []map[string]any
}

func (s *AslanHTTPDashboardSource) countryFilter(ctx context.Context, query StatisticsQuery) (externalDashboardCountryFilter, error) {
	if query.CountryID > 0 {
		return externalDashboardCountryFilter{}, fmt.Errorf("aslan dashboard source %s does not support numeric country_id filter", s.appCode)
	}
	if query.RegionID <= 0 {
		return externalDashboardCountryFilter{}, nil
	}
	for _, resolver := range s.regionResolvers {
		codes, handled, err := resolver.CountryCodesForRegion(ctx, s.appCode, query.RegionID)
		if err != nil {
			return externalDashboardCountryFilter{}, err
		}
		if !handled {
			continue
		}
		return externalDashboardCountryFilter{
			Applied:  true,
			RegionID: query.RegionID,
			Codes:    normalizeExternalCountryCodes(codes),
		}, nil
	}
	// Aslan 接口返回的是 legacy 区域字符串；admin 前端使用的 region_id 可能是 Mongo ObjectId 的稳定 int64 映射。
	// 缺 legacy 映射时才回退尝试远端 regionId 数字值，兼容历史区域本身就是数字字符串的环境。
	return externalDashboardCountryFilter{RegionID: query.RegionID}, nil
}

func (s *AslanHTTPDashboardSource) loadRange(ctx context.Context, startDate time.Time, endDate time.Time, countryFilter externalDashboardCountryFilter) (aslanRangeOverview, error) {
	totalFlow := aslanDashboardMetric{}
	totalSnapshot := aslanDashboardMetric{}
	dailySeries := []map[string]any{}
	dailyCountries := []map[string]any{}
	countryAggregates := map[string]*aslanCountryAccumulator{}
	for day := startDate; day.Before(endDate); day = day.AddDate(0, 0, 1) {
		regions, err := s.fetchDay(ctx, day)
		if err != nil {
			return aslanRangeOverview{}, err
		}
		dayFlow := aslanDashboardMetric{}
		daySnapshot := aslanDashboardMetric{}
		dayCountries := map[string]*aslanCountryAccumulator{}
		for _, region := range regions {
			if !s.regionIncluded(region, countryFilter) {
				continue
			}
			for _, country := range region.Countries {
				countryCode := strings.ToUpper(strings.TrimSpace(country.CountryCode))
				if countryCode == "" || !countryIncluded(countryCode, countryFilter) {
					continue
				}
				metric, err := aslanCountryMetric(country)
				if err != nil {
					return aslanRangeOverview{}, fmt.Errorf("map aslan country %s on %s: %w", countryCode, dashboardSQLDate(day), err)
				}
				dayFlow.addFlow(metric)
				daySnapshot.addSnapshot(metric)
				totalFlow.addFlow(metric)
				dayAggregate := dayCountries[countryCode]
				if dayAggregate == nil {
					dayAggregate = &aslanCountryAccumulator{CountryCode: countryCode}
					dayCountries[countryCode] = dayAggregate
				}
				dayAggregate.addCountry(region, country, metric, countryFilter.RegionID)
			}
		}
		totalSnapshot = daySnapshot
		dayMetric := aslanCombinedMetric(dayFlow, daySnapshot)
		dayRow := dayMetric.toMap()
		dayLabel := dashboardSQLDate(day)
		dayRow["label"] = dayLabel
		dayRow["stat_day"] = dayLabel
		applyAslanUnavailableFields(dayRow)
		dailySeries = append(dailySeries, dayRow)
		mergeAslanCountryDay(countryAggregates, dayCountries)
		dailyCountries = append(dailyCountries, aslanDailyCountryRows(dayLabel, dayCountries)...)
	}
	return aslanRangeOverview{
		Total:                 aslanCombinedMetric(totalFlow, totalSnapshot),
		DailySeries:           dailySeries,
		CountryBreakdown:      aslanCountryRows(countryAggregates),
		DailyCountryBreakdown: dailyCountries,
	}, nil
}

func (s *AslanHTTPDashboardSource) fetchDay(ctx context.Context, day time.Time) ([]aslanRegionStatistics, error) {
	regions, err := s.fetchDayWithDateParam(ctx, day, dashboardSQLDate(day))
	if err == nil {
		return regions, nil
	}
	var dateErr aslanDateParameterError
	if !errors.As(err, &dateErr) {
		return nil, err
	}
	// 线上 Aslan 旧版本把 LocalDate 参数按 yyyyMMdd 转换；当前代码标注是 ISO date。这里只在明确日期转换失败时降级，兼容两版服务。
	return s.fetchDayWithDateParam(ctx, day, day.Format("20060102"))
}

func (s *AslanHTTPDashboardSource) fetchDayWithDateParam(ctx context.Context, day time.Time, dateParam string) ([]aslanRegionStatistics, error) {
	endpoint, err := url.Parse(s.baseURL + s.overviewPath)
	if err != nil {
		return nil, fmt.Errorf("build aslan dashboard request url: %w", err)
	}
	values := endpoint.Query()
	values.Set("date", dateParam)
	endpoint.RawQuery = values.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
	if err != nil {
		return nil, err
	}
	resp, err := s.client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request aslan dashboard %s: %w", dashboardSQLDate(day), err)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		if resp.StatusCode == http.StatusBadRequest && bytes.Contains(body, []byte("LocalDate")) {
			return nil, aslanDateParameterError{dateParam: dateParam}
		}
		return nil, fmt.Errorf("aslan dashboard returned status %d for %s: %s", resp.StatusCode, dashboardSQLDate(day), truncateExternalDashboardError(body))
	}
	regions, err := decodeAslanRegions(body)
	if err != nil {
		return nil, fmt.Errorf("decode aslan dashboard %s: %w", dashboardSQLDate(day), err)
	}
	return regions, nil
}

type aslanDateParameterError struct {
	dateParam string
}

func (e aslanDateParameterError) Error() string {
	return "aslan dashboard date parameter rejected: " + e.dateParam
}

func truncateExternalDashboardError(body []byte) string {
	text := strings.TrimSpace(string(body))
	if len(text) > 300 {
		return text[:300]
	}
	return text
}

func decodeAslanRegions(body []byte) ([]aslanRegionStatistics, error) {
	trimmed := bytes.TrimSpace(body)
	if len(trimmed) == 0 {
		return nil, nil
	}
	if trimmed[0] == '[' {
		var regions []aslanRegionStatistics
		if err := json.Unmarshal(trimmed, &regions); err != nil {
			return nil, err
		}
		return regions, nil
	}
	var envelope map[string]json.RawMessage
	if err := json.Unmarshal(trimmed, &envelope); err != nil {
		return nil, err
	}
	for _, key := range []string{"data", "result", "body"} {
		raw := bytes.TrimSpace(envelope[key])
		if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
			continue
		}
		var regions []aslanRegionStatistics
		if err := json.Unmarshal(raw, &regions); err != nil {
			return nil, err
		}
		return regions, nil
	}
	return nil, fmt.Errorf("missing array payload")
}

func (s *AslanHTTPDashboardSource) regionIncluded(region aslanRegionStatistics, countryFilter externalDashboardCountryFilter) bool {
	if countryFilter.Applied || countryFilter.RegionID <= 0 {
		return true
	}
	regionID, err := strconv.ParseInt(strings.TrimSpace(region.RegionID), 10, 64)
	return err == nil && regionID == countryFilter.RegionID
}

func countryIncluded(countryCode string, countryFilter externalDashboardCountryFilter) bool {
	if !countryFilter.Applied {
		return true
	}
	for _, code := range countryFilter.Codes {
		if code == countryCode {
			return true
		}
	}
	return false
}

func aslanCountryMetric(country aslanCountryStatistics) (aslanDashboardMetric, error) {
	recharge, err := country.DailyRecharge.Minor()
	if err != nil {
		return aslanDashboardMetric{}, err
	}
	salary, err := country.HostSalaryBalance.Minor()
	if err != nil {
		return aslanDashboardMetric{}, err
	}
	return aslanDashboardMetric{
		RechargeUSDMinor: recharge,
		ActiveUsers:      country.DailyActive.Int64(),
		NewUsers:         country.DailyRegister.Int64(),
		CoinTotal:        country.GoldBalance.Int64(),
		SalaryUSDMinor:   salary,
	}, nil
}

func aslanCombinedMetric(flow aslanDashboardMetric, snapshot aslanDashboardMetric) externalDashboardMetric {
	metric := externalDashboardMetric{
		NewUsers:              flow.NewUsers,
		ActiveUsers:           flow.ActiveUsers,
		PaidUsers:             flow.PaidUsers,
		UserRechargeUSDMinor:  flow.RechargeUSDMinor,
		RechargeUSDMinor:      flow.RechargeUSDMinor,
		CoinTotal:             int64Ptr(snapshot.CoinTotal),
		SalaryUSDMinor:        int64Ptr(snapshot.SalaryUSDMinor),
		D1RetentionUsers:      0,
		D1RetentionBaseUsers:  0,
		D7RetentionUsers:      0,
		D7RetentionBaseUsers:  0,
		D30RetentionUsers:     0,
		D30RetentionBaseUsers: 0,
	}
	metric.finalize()
	return metric
}

func (m *aslanDashboardMetric) addFlow(item aslanDashboardMetric) {
	m.RechargeUSDMinor += item.RechargeUSDMinor
	m.ActiveUsers += item.ActiveUsers
	m.NewUsers += item.NewUsers
	m.PaidUsers += item.PaidUsers
}

func (m *aslanDashboardMetric) addSnapshot(item aslanDashboardMetric) {
	m.CoinTotal += item.CoinTotal
	m.SalaryUSDMinor += item.SalaryUSDMinor
}

func (a *aslanCountryAccumulator) addCountry(region aslanRegionStatistics, country aslanCountryStatistics, metric aslanDashboardMetric, requestRegionID int64) {
	a.CountryName = firstExternalDashboardValue(country.CountryName, a.CountryName, a.CountryCode)
	a.RegionCode = firstExternalDashboardValue(region.RegionCode, a.RegionCode)
	a.RegionName = firstExternalDashboardValue(region.RegionName, a.RegionName)
	if requestRegionID > 0 {
		a.RegionID = requestRegionID
	} else if a.RegionID == 0 {
		if regionID, err := strconv.ParseInt(strings.TrimSpace(region.RegionID), 10, 64); err == nil {
			a.RegionID = regionID
		}
	}
	a.Flow.addFlow(metric)
	a.Snapshot.addSnapshot(metric)
}

func mergeAslanCountryDay(total map[string]*aslanCountryAccumulator, day map[string]*aslanCountryAccumulator) {
	for countryCode, dayCountry := range day {
		aggregate := total[countryCode]
		if aggregate == nil {
			aggregate = &aslanCountryAccumulator{
				CountryCode: dayCountry.CountryCode,
				CountryName: dayCountry.CountryName,
				RegionID:    dayCountry.RegionID,
				RegionCode:  dayCountry.RegionCode,
				RegionName:  dayCountry.RegionName,
			}
			total[countryCode] = aggregate
		}
		aggregate.CountryName = firstExternalDashboardValue(dayCountry.CountryName, aggregate.CountryName, aggregate.CountryCode)
		aggregate.RegionID = maxPositiveInt64(aggregate.RegionID, dayCountry.RegionID)
		aggregate.RegionCode = firstExternalDashboardValue(dayCountry.RegionCode, aggregate.RegionCode)
		aggregate.RegionName = firstExternalDashboardValue(dayCountry.RegionName, aggregate.RegionName)
		aggregate.Flow.addFlow(dayCountry.Flow)
		// 金币和工资是 Aslan 接口提供的余额快照，区间国家明细必须使用最后一天快照，不能按天累加成虚高余额。
		aggregate.Snapshot = dayCountry.Snapshot
	}
}

func maxPositiveInt64(left int64, right int64) int64 {
	if left > 0 {
		return left
	}
	return right
}

func aslanCountryRows(countries map[string]*aslanCountryAccumulator) []map[string]any {
	rows := make([]map[string]any, 0, len(countries))
	for _, country := range countries {
		rows = append(rows, aslanCountryRow("", country))
	}
	sort.Slice(rows, func(i, j int) bool {
		left, _ := rows[i]["recharge_usd_minor"].(int64)
		right, _ := rows[j]["recharge_usd_minor"].(int64)
		if left == right {
			return fmt.Sprint(rows[i]["country_code"]) < fmt.Sprint(rows[j]["country_code"])
		}
		return left > right
	})
	return rows
}

func aslanDailyCountryRows(statDay string, countries map[string]*aslanCountryAccumulator) []map[string]any {
	keys := make([]string, 0, len(countries))
	for key := range countries {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	rows := make([]map[string]any, 0, len(keys))
	for _, key := range keys {
		rows = append(rows, aslanCountryRow(statDay, countries[key]))
	}
	return rows
}

func aslanCountryRow(statDay string, country *aslanCountryAccumulator) map[string]any {
	metric := aslanCombinedMetric(country.Flow, country.Snapshot)
	row := metric.toMap()
	if statDay != "" {
		row["label"] = statDay
		row["stat_day"] = statDay
	}
	row["country_id"] = 0
	row["country_code"] = country.CountryCode
	row["country_name"] = firstExternalDashboardValue(country.CountryName, country.CountryCode)
	row["country"] = firstExternalDashboardValue(country.CountryName, country.CountryCode)
	if country.RegionID > 0 {
		row["region_id"] = country.RegionID
	}
	if country.RegionCode != "" {
		row["region_code"] = country.RegionCode
	}
	if country.RegionName != "" {
		row["region_name"] = country.RegionName
	}
	applyAslanUnavailableFields(row)
	return row
}

func (v *aslanDecimal) UnmarshalJSON(data []byte) error {
	raw := strings.TrimSpace(string(data))
	if raw == "" || raw == "null" {
		v.raw = "0"
		return nil
	}
	if strings.HasPrefix(raw, `"`) {
		var decoded string
		if err := json.Unmarshal(data, &decoded); err != nil {
			return err
		}
		raw = decoded
	}
	v.raw = strings.TrimSpace(raw)
	if v.raw == "" {
		v.raw = "0"
	}
	return nil
}

func (v aslanDecimal) Minor() (int64, error) {
	return decimalTextToMinor(v.raw)
}

func (v *aslanInt64) UnmarshalJSON(data []byte) error {
	raw := strings.TrimSpace(string(data))
	if raw == "" || raw == "null" {
		v.value = 0
		return nil
	}
	if strings.HasPrefix(raw, `"`) {
		var decoded string
		if err := json.Unmarshal(data, &decoded); err != nil {
			return err
		}
		raw = decoded
	}
	parsed, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
	if err != nil {
		return fmt.Errorf("parse int64 %q: %w", raw, err)
	}
	v.value = parsed
	return nil
}

func (v aslanInt64) Int64() int64 {
	return v.value
}

func nullableMetricInt64(value *int64) int64 {
	if value == nil {
		return 0
	}
	return *value
}

var aslanUnavailableMetricFields = []string{
	"paid_users",
	"recharge_users",
	"new_user_recharge_usd_minor",
	"coin_seller_recharge_usd_minor",
	"coin_seller_stock_coin",
	"coin_seller_transfer_coin",
	"google_recharge_usd_minor",
	"mifapay_recharge_usd_minor",
	"game_turnover",
	"game_payout",
	"game_players",
	"game_profit",
	"game_profit_rate",
	"lucky_gift_turnover",
	"lucky_gift_payout",
	"lucky_gift_payers",
	"lucky_gift_profit",
	"lucky_gift_profit_rate",
	"super_lucky_gift_turnover",
	"super_lucky_gift_payout",
	"super_lucky_gift_profit",
	"super_lucky_gift_profit_rate",
	"gift_coin_spent",
	"consumed_coin",
	"output_coin",
	"consume_output_ratio",
	"platform_grant_coin",
	"manual_grant_coin",
	"salary_transfer_coin",
	"mic_online_ms",
	"avg_mic_online_ms",
	"arppu_usd_minor",
	"payer_rate",
	"paid_conversion_rate",
	"recharge_conversion_rate",
	"arppu_delta_rate",
	"paid_users_delta_rate",
	"payer_rate_delta_pp",
	"paid_conversion_rate_delta_pp",
	"recharge_conversion_rate_delta_pp",
	"gift_coin_spent_delta_rate",
	"game_turnover_delta_rate",
	"game_profit_delta_rate",
	"lucky_gift_turnover_delta_rate",
	"lucky_gift_profit_delta_rate",
	"d1_retention_rate",
	"d7_retention_rate",
	"d30_retention_rate",
}

func applyAslanUnavailableFields(row map[string]any) {
	for _, field := range aslanUnavailableMetricFields {
		row[field] = nil
	}
}

func aslanRetentionMap(metric externalDashboardMetric) map[string]any {
	return map[string]any{
		"registered_users": metric.NewUsers,
		"day1_base_users":  nil,
		"day1_rate":        nil,
		"day1_users":       nil,
		"day7_base_users":  nil,
		"day7_rate":        nil,
		"day7_users":       nil,
		"day30_base_users": nil,
		"day30_rate":       nil,
		"day30_users":      nil,
	}
}

func aslanDashboardMetricSources() []map[string]any {
	return []map[string]any{
		{"field": "new_users", "available": true, "source": "Aslan /console/datav/aslan/region-country/statistics dailyRegister"},
		{"field": "active_users", "available": true, "source": "Aslan /console/datav/aslan/region-country/statistics dailyActive"},
		{"field": "paid_users", "available": false, "missing_reason": "Aslan 当前接口没有付费用户数"},
		{"field": "recharge_users", "available": false, "missing_reason": "Aslan 当前接口没有充值用户数"},
		{"field": "recharge_usd_minor", "available": true, "source": "Aslan /console/datav/aslan/region-country/statistics dailyRecharge"},
		{"field": "new_user_recharge_usd_minor", "available": false, "missing_reason": "Aslan 当前接口没有新用户充值字段"},
		{"field": "coin_seller_recharge_usd_minor", "available": false, "missing_reason": "Aslan 当前接口没有币商充值金额字段"},
		{"field": "coin_seller_stock_coin", "available": false, "missing_reason": "Aslan 当前接口没有币商充值金币字段"},
		{"field": "coin_seller_transfer_coin", "available": false, "missing_reason": "Aslan 当前接口没有币商出货金币字段"},
		{"field": "google_recharge_usd_minor", "available": false, "missing_reason": "Aslan 当前接口没有 Google 充值拆分字段"},
		{"field": "mifapay_recharge_usd_minor", "available": false, "missing_reason": "Aslan 当前接口没有三方充值拆分字段"},
		{"field": "game_turnover", "available": false, "missing_reason": "Aslan 当前接口没有游戏流水和返奖字段"},
		{"field": "lucky_gift_turnover", "available": false, "missing_reason": "Aslan 当前接口没有幸运礼物流水和返奖字段"},
		{"field": "super_lucky_gift_turnover", "available": false, "missing_reason": "Aslan 当前接口没有超级幸运礼物字段"},
		{"field": "gift_coin_spent", "available": false, "missing_reason": "Aslan 当前接口没有礼物流水字段"},
		{"field": "coin_total", "available": true, "source": "Aslan /console/datav/aslan/region-country/statistics goldBalance"},
		{"field": "consumed_coin", "available": false, "missing_reason": "Aslan 当前接口没有消耗金币总口径"},
		{"field": "output_coin", "available": false, "missing_reason": "Aslan 当前接口没有产出金币总口径"},
		{"field": "consume_output_ratio", "available": false, "missing_reason": "缺少消耗金币和产出金币总口径"},
		{"field": "platform_grant_coin", "available": false, "missing_reason": "Aslan 当前接口没有平台发放金币字段"},
		{"field": "manual_grant_coin", "available": false, "missing_reason": "Aslan 当前接口没有人工发放金币字段"},
		{"field": "salary_usd_minor", "available": true, "source": "Aslan /console/datav/aslan/region-country/statistics hostSalaryBalance"},
		{"field": "salary_transfer_coin", "available": false, "missing_reason": "Aslan 当前接口没有工资兑换金币字段"},
		{"field": "avg_mic_online_ms", "available": false, "missing_reason": "Aslan 当前接口没有麦上时长字段"},
		{"field": "mic_online_ms", "available": false, "missing_reason": "Aslan 当前接口没有麦上时长字段"},
		{"field": "arpu_usd_minor", "available": true, "source": "dailyRecharge / dailyActive"},
		{"field": "arppu_usd_minor", "available": false, "missing_reason": "Aslan 当前接口没有付费用户数，不能计算 ARPPU"},
		{"field": "paid_conversion_rate", "available": false, "missing_reason": "Aslan 当前接口没有付费用户数"},
		{"field": "recharge_conversion_rate", "available": false, "missing_reason": "Aslan 当前接口没有充值用户数"},
		{"field": "retention.day1_rate", "available": false, "missing_reason": "Aslan 当前接口没有次日留存 cohort 字段"},
		{"field": "retention.day7_rate", "available": false, "missing_reason": "Aslan 当前接口没有 7 日留存 cohort 字段"},
		{"field": "retention.day30_rate", "available": false, "missing_reason": "Aslan 当前接口没有 30 日留存 cohort 字段"},
	}
}
