package com.red.circle.console.app.service.app.datav;

import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.console.app.dto.clientobject.datav.CountryDashboardGameMetricCO;
import com.red.circle.console.app.dto.cmd.datav.CountryDashboardGameMetricQryCmd;
import com.red.circle.console.infra.database.rds.dao.datav.CountryDashboardDAO;
import com.red.circle.console.infra.database.rds.dto.datav.CountryDashboardGameMetricDTO;
import com.red.circle.framework.dto.PageResult;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

/**
 * 国家数据大屏游戏维度预聚合.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CountryDashboardGameMetricServiceImpl implements CountryDashboardGameMetricService {

  private static final String PERIOD_DAY = "DAY";
  private static final String PERIOD_WEEK = "WEEK";
  private static final String PERIOD_MONTH = "MONTH";
  private static final String PERIOD_ALL = "ALL";
  private static final ZoneId STORAGE_ZONE_ID = ZoneId.of("Asia/Riyadh");
  private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
  private static final DateTimeFormatter MONTH_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM");
  private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  private static final WeekFields WEEK_FIELDS = WeekFields.ISO;

  private final CountryDashboardDAO countryDashboardDAO;
  private final TransactionTemplate transactionTemplate;

  @Value("${red-circle.country-dashboard.refresh-sys-origins:LIKEI}")
  private String refreshSysOrigins;

  @Value("${red-circle.country-dashboard.refresh-timezones:Asia/Riyadh,UTC,Asia/Shanghai}")
  private String refreshTimezones;

  private volatile boolean metricSchemaEnsured;

  @Override
  public PageResult<CountryDashboardGameMetricCO> pageGameMetrics(
      CountryDashboardGameMetricQryCmd cmd) {
    ensureMetricSchema();
    QueryCondition condition = QueryCondition.of(cmd);
    int cursor = Math.max(1, cmd == null || cmd.getCursor() == null ? 1 : cmd.getCursor());
    int limit = Math.max(1, cmd == null || cmd.getLimit() == null ? 50 : cmd.getLimit());
    limit = Math.min(200, limit);
    int offset = (cursor - 1) * limit;

    PageResult<CountryDashboardGameMetricCO> result = new PageResult<>();
    result.setCurrent(cursor);
    result.setSize(limit);
    if (!hasText(condition.sysOrigin)) {
      result.setTotal(0L);
      result.setRecords(List.of());
      return result;
    }

    SortSpec sortSpec = normalizeSort(cmd == null ? null : cmd.getSortField(),
        cmd == null ? null : cmd.getSortOrder());
    String countryCodes = normalizeCsv(cmd == null ? null : cmd.getCountryCodes());
    String gameProvider = normalizeGameProvider(cmd == null ? null : cmd.getGameProvider());
    String gameKeyword = trimToNull(cmd == null ? null : cmd.getGameKeyword());

    Long total = countryDashboardDAO.countGamePeriodMetrics(
        condition.periodType, condition.startTime, condition.endTime,
        PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null,
        condition.startPeriodKey, condition.endPeriodKey, condition.statTimezone,
        condition.countryKeyword, countryCodes, gameProvider, gameKeyword, condition.sysOrigin);
    List<CountryDashboardGameMetricCO> records = countryDashboardDAO.listGamePeriodMetrics(
            condition.periodType, condition.startTime, condition.endTime,
            PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null,
            condition.startPeriodKey, condition.endPeriodKey, condition.statTimezone,
            condition.countryKeyword, countryCodes, gameProvider, gameKeyword, condition.sysOrigin,
            sortSpec.column(), sortSpec.direction(), offset, limit)
        .stream()
        .map(this::toCO)
        .toList();

    result.setTotal(total == null ? 0L : total);
    result.setRecords(records);
    return result;
  }

  @Override
  public PageResult<CountryDashboardGameMetricCO> pageGameSummaries(
      CountryDashboardGameMetricQryCmd cmd) {
    ensureMetricSchema();
    QueryCondition condition = QueryCondition.of(cmd);
    int cursor = Math.max(1, cmd == null || cmd.getCursor() == null ? 1 : cmd.getCursor());
    int limit = Math.max(1, cmd == null || cmd.getLimit() == null ? 50 : cmd.getLimit());
    limit = Math.min(200, limit);
    int offset = (cursor - 1) * limit;

    PageResult<CountryDashboardGameMetricCO> result = new PageResult<>();
    result.setCurrent(cursor);
    result.setSize(limit);
    if (!hasText(condition.sysOrigin)) {
      result.setTotal(0L);
      result.setRecords(List.of());
      return result;
    }

    SortSpec sortSpec = normalizeSummarySort(cmd == null ? null : cmd.getSortField(),
        cmd == null ? null : cmd.getSortOrder());
    String countryCodes = normalizeCsv(cmd == null ? null : cmd.getCountryCodes());
    String gameProvider = normalizeGameProvider(cmd == null ? null : cmd.getGameProvider());
    String gameKeyword = trimToNull(cmd == null ? null : cmd.getGameKeyword());

    Long total = countryDashboardDAO.countGameSummaryMetrics(
        condition.periodType, condition.startTime, condition.endTime,
        PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null,
        condition.startPeriodKey, condition.endPeriodKey, condition.statTimezone,
        condition.countryKeyword, countryCodes, gameProvider, gameKeyword, condition.sysOrigin);
    List<CountryDashboardGameMetricCO> records = countryDashboardDAO.listGameSummaryMetrics(
            condition.periodType, condition.startTime, condition.endTime,
            PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null,
            condition.startPeriodKey, condition.endPeriodKey, condition.statTimezone,
            condition.countryKeyword, countryCodes, gameProvider, gameKeyword, condition.sysOrigin,
            sortSpec.column(), sortSpec.direction(), offset, limit)
        .stream()
        .map(this::toCO)
        .toList();

    result.setTotal(total == null ? 0L : total);
    result.setRecords(records);
    return result;
  }

  @Override
  public List<CountryDashboardGameMetricCO> listGameCountryRanks(
      CountryDashboardGameMetricQryCmd cmd) {
    ensureMetricSchema();
    QueryCondition condition = QueryCondition.of(cmd);
    if (!hasText(condition.sysOrigin)) {
      return List.of();
    }
    String gameProvider = normalizeGameProvider(cmd == null ? null : cmd.getGameProvider());
    String gameId = trimToNull(cmd == null ? null : cmd.getGameId());
    if (gameProvider == null || gameId == null) {
      return List.of();
    }
    int limit = Math.max(1, cmd.getRankLimit() == null ? 100 : cmd.getRankLimit());
    limit = Math.min(500, limit);
    String countryCodes = normalizeCsv(cmd.getCountryCodes());
    return countryDashboardDAO.listGameCountryRankMetrics(
            condition.periodType, condition.startTime, condition.endTime,
            PERIOD_ALL.equals(condition.periodType) ? PERIOD_ALL : null,
            condition.startPeriodKey, condition.endPeriodKey, condition.statTimezone,
            condition.countryKeyword, countryCodes, gameProvider, null, gameId, condition.sysOrigin,
            limit)
        .stream()
        .map(this::toCO)
        .toList();
  }

  @Override
  public void refreshRecentDays(int days) {
    ensureMetricSchema();
    int safeDays = Math.max(days, 1);
    List<String> sysOrigins = resolveSysOrigins();
    List<StatTimezone> statTimezones = resolveStatTimezones(null);
    for (String sysOrigin : sysOrigins) {
      for (StatTimezone statTimezone : statTimezones) {
        List<LocalDate> statDates = recentDates(safeDays, statTimezone.zoneId());
        for (LocalDate statDate : statDates) {
          try {
            refreshPeriod(toDayWindow(statDate), sysOrigin, statTimezone);
          } catch (Exception ex) {
            log.error("refresh country dashboard game day metric failed, statDate={}, sysOrigin={}, statTimezone={}",
                statDate, sysOrigin, statTimezone.id(), ex);
          }
        }
        refreshAffectedPeriods(statDates, sysOrigin, statTimezone, false);
        try {
          refreshAll(sysOrigin, statTimezone);
        } catch (Exception ex) {
          log.error("refresh country dashboard game all metric failed, sysOrigin={}, statTimezone={}",
              sysOrigin, statTimezone.id(), ex);
        }
      }
    }
  }

  @Override
  public List<String> refreshRange(LocalDate startDate, LocalDate endDate, String sysOrigin,
      String statTimezonesValue) {
    ensureMetricSchema();
    if (startDate == null || endDate == null) {
      throw new IllegalArgumentException("startDate and endDate are required");
    }
    if (endDate.isBefore(startDate)) {
      throw new IllegalArgumentException("endDate must be greater than or equal to startDate");
    }
    long dayCount = ChronoUnit.DAYS.between(startDate, endDate) + 1;
    if (dayCount > 366) {
      throw new IllegalArgumentException("backfill range can not exceed 366 days");
    }
    String safeSysOrigin = trimToNull(sysOrigin);
    if (safeSysOrigin == null) {
      safeSysOrigin = SysOriginPlatformEnum.LIKEI.name();
    }

    List<LocalDate> statDates = new ArrayList<>();
    for (long i = 0; i < dayCount; i++) {
      statDates.add(startDate.plusDays(i));
    }
    List<StatTimezone> statTimezones = resolveStatTimezones(statTimezonesValue);
    long start = System.currentTimeMillis();
    log.info("refresh country dashboard game range start, startDate={}, endDate={}, days={}, sysOrigin={}, statTimezones={}",
        startDate, endDate, dayCount, safeSysOrigin,
        statTimezones.stream().map(StatTimezone::id).toList());
    for (StatTimezone statTimezone : statTimezones) {
      for (LocalDate statDate : statDates) {
        refreshPeriod(toDayWindow(statDate), safeSysOrigin, statTimezone);
      }
      refreshAffectedPeriods(statDates, safeSysOrigin, statTimezone, true);
      refreshAll(safeSysOrigin, statTimezone);
    }
    log.info("refresh country dashboard game range success, startDate={}, endDate={}, days={}, sysOrigin={}, statTimezones={}, costMs={}",
        startDate, endDate, dayCount, safeSysOrigin,
        statTimezones.stream().map(StatTimezone::id).toList(), System.currentTimeMillis() - start);
    return statTimezones.stream().map(StatTimezone::id).toList();
  }

  private void ensureMetricSchema() {
    if (metricSchemaEnsured) {
      return;
    }
    synchronized (this) {
      if (metricSchemaEnsured) {
        return;
      }
      countryDashboardDAO.createGamePeriodMetricTable();
      countryDashboardDAO.createGamePeriodUserMetricTable();
      metricSchemaEnsured = true;
      log.info("country dashboard game metric tables ensured");
    }
  }

  private void refreshAffectedPeriods(List<LocalDate> statDates, String sysOrigin,
      StatTimezone statTimezone, boolean failFast) {
    Set<PeriodWindow> windows = new LinkedHashSet<>();
    for (LocalDate statDate : statDates) {
      windows.add(toWeekWindow(statDate, statTimezone.zoneId()));
      windows.add(toMonthWindow(statDate, statTimezone.zoneId()));
    }
    for (PeriodWindow window : windows) {
      try {
        refreshPeriod(window, sysOrigin, statTimezone);
      } catch (Exception ex) {
        if (failFast) {
          throw ex;
        }
        log.error("refresh country dashboard game period metric failed, periodType={}, periodKey={}, sysOrigin={}, statTimezone={}",
            window.periodType(), window.periodKey(), sysOrigin, statTimezone.id(), ex);
      }
    }
  }

  private void refreshPeriod(PeriodWindow window, String sysOrigin, StatTimezone statTimezone) {
    long start = System.currentTimeMillis();
    TimeWindow timeWindow = toStorageTimeWindow(window, statTimezone);
    Map<String, CountryDashboardGameMetricDTO> rows = new LinkedHashMap<>();
    mergeAmounts(rows, countryDashboardDAO.listWalletGameMetricAmounts(
        window.periodType(), timeWindow.startTime(), timeWindow.endTime(),
        timeWindow.startTimeMillis(), timeWindow.endTimeMillis(), null, sysOrigin,
        zoneOffset(STORAGE_ZONE_ID), statTimezone.offset()), window, sysOrigin, statTimezone);
    mergeAmounts(rows, countryDashboardDAO.listLuckyBoxGameMetricAmounts(
        window.periodType(), timeWindow.startTime(), timeWindow.endTime(), null, sysOrigin,
        zoneOffset(STORAGE_ZONE_ID), statTimezone.offset()), window, sysOrigin, statTimezone);

    RefreshWriteResult writeResult = transactionTemplate.execute(status -> {
      countryDashboardDAO.deleteGamePeriodUserMetrics(window.periodType(), window.periodKey(),
          statTimezone.id(), sysOrigin);
      int walletUsers = countryDashboardDAO.insertWalletGamePeriodUserMetrics(
          window.periodType(), window.periodKey(), window.periodName(),
          window.periodStartDate(), window.periodEndDate(), statTimezone.id(),
          timeWindow.startTime(), timeWindow.endTime(), timeWindow.startTimeMillis(),
          timeWindow.endTimeMillis(), sysOrigin);
      int luckyBoxUsers = countryDashboardDAO.insertLuckyBoxGamePeriodUserMetrics(
          window.periodType(), window.periodKey(), window.periodName(),
          window.periodStartDate(), window.periodEndDate(), statTimezone.id(),
          timeWindow.startTime(), timeWindow.endTime(), sysOrigin);
      mergeUserCounts(rows, countryDashboardDAO.listGamePeriodUserMetricCounts(
          window.periodType(), window.periodKey(), statTimezone.id(), sysOrigin),
          window, sysOrigin, statTimezone);
      List<CountryDashboardGameMetricDTO> metrics = new ArrayList<>(rows.values());
      int deleted = countryDashboardDAO.deleteGamePeriodMetrics(window.periodType(),
          window.periodKey(), statTimezone.id(), sysOrigin);
      int inserted = metrics.isEmpty() ? 0 : countryDashboardDAO.upsertGamePeriodMetrics(metrics);
      return new RefreshWriteResult(deleted, inserted, walletUsers + luckyBoxUsers);
    });

    log.info("refresh country dashboard game period metric success, periodType={}, periodKey={}, sysOrigin={}, statTimezone={}, rows={}, deleted={}, inserted={}, userRows={}, costMs={}",
        window.periodType(), window.periodKey(), sysOrigin, statTimezone.id(), rows.size(),
        writeResult.deleted(), writeResult.inserted(), writeResult.userRows(),
        System.currentTimeMillis() - start);
  }

  private void refreshAll(String sysOrigin, StatTimezone statTimezone) {
    long start = System.currentTimeMillis();
    PeriodWindow window = new PeriodWindow(PERIOD_ALL, PERIOD_ALL, "全部", null, null);
    Map<String, CountryDashboardGameMetricDTO> rows = new LinkedHashMap<>();
    mergeAmounts(rows, countryDashboardDAO.listAllGamePeriodAmountsFromPeriodDays(
        statTimezone.id(), sysOrigin), window, sysOrigin, statTimezone);

    RefreshWriteResult writeResult = transactionTemplate.execute(status -> {
      countryDashboardDAO.deleteGamePeriodUserMetrics(PERIOD_ALL, PERIOD_ALL,
          statTimezone.id(), sysOrigin);
      int userRows = countryDashboardDAO.insertAllGamePeriodUserMetricsFromDaily(
          statTimezone.id(), sysOrigin);
      mergeUserCounts(rows, countryDashboardDAO.listGamePeriodUserMetricCounts(
          PERIOD_ALL, PERIOD_ALL, statTimezone.id(), sysOrigin), window, sysOrigin, statTimezone);
      List<CountryDashboardGameMetricDTO> metrics = new ArrayList<>(rows.values());
      int deleted = countryDashboardDAO.deleteGamePeriodMetrics(PERIOD_ALL, PERIOD_ALL,
          statTimezone.id(), sysOrigin);
      int inserted = metrics.isEmpty() ? 0 : countryDashboardDAO.upsertGamePeriodMetrics(metrics);
      return new RefreshWriteResult(deleted, inserted, userRows);
    });

    log.info("refresh country dashboard game all metric success, sysOrigin={}, statTimezone={}, rows={}, deleted={}, inserted={}, userRows={}, costMs={}",
        sysOrigin, statTimezone.id(), rows.size(), writeResult.deleted(), writeResult.inserted(),
        writeResult.userRows(), System.currentTimeMillis() - start);
  }

  private void mergeAmounts(Map<String, CountryDashboardGameMetricDTO> rows,
      List<CountryDashboardGameMetricDTO> metrics, PeriodWindow window, String sysOrigin,
      StatTimezone statTimezone) {
    if (metrics == null || metrics.isEmpty()) {
      return;
    }
    for (CountryDashboardGameMetricDTO metric : metrics) {
      CountryDashboardGameMetricDTO row = getRow(rows, metric, window, sysOrigin, statTimezone);
      row.setConsumeAmount(add(row.getConsumeAmount(), metric.getConsumeAmount()));
      row.setPayoutAmount(add(row.getPayoutAmount(), metric.getPayoutAmount()));
      row.setProfitAmount(add(row.getProfitAmount(), metric.getProfitAmount()));
      row.setOrderCount(safeLong(row.getOrderCount()) + safeLong(metric.getOrderCount()));
    }
  }

  private void mergeUserCounts(Map<String, CountryDashboardGameMetricDTO> rows,
      List<CountryDashboardGameMetricDTO> metrics, PeriodWindow window, String sysOrigin,
      StatTimezone statTimezone) {
    if (metrics == null || metrics.isEmpty()) {
      return;
    }
    for (CountryDashboardGameMetricDTO metric : metrics) {
      CountryDashboardGameMetricDTO row = getRow(rows, metric, window, sysOrigin, statTimezone);
      row.setUserCount(safeLong(metric.getUserCount()));
    }
  }

  private CountryDashboardGameMetricDTO getRow(
      Map<String, CountryDashboardGameMetricDTO> rows, CountryDashboardGameMetricDTO metric,
      PeriodWindow window, String sysOrigin, StatTimezone statTimezone) {
    String countryCode = hasText(metric.getCountryCode()) ? metric.getCountryCode() : "UNKNOWN";
    String countryName = hasText(metric.getCountryName()) ? metric.getCountryName() : countryCode;
    String gameProvider = hasText(metric.getGameProvider()) ? metric.getGameProvider() : "UNKNOWN";
    String gameId = hasText(metric.getGameId()) ? metric.getGameId() : "UNKNOWN";
    String gameName = hasText(metric.getGameName()) ? metric.getGameName() : gameId;
    return rows.computeIfAbsent(countryCode + "|" + gameProvider + "|" + gameId,
        key -> new CountryDashboardGameMetricDTO()
            .setPeriodType(window.periodType())
            .setPeriodKey(window.periodKey())
            .setStatTimezone(statTimezone.id())
            .setPeriodName(window.periodName())
            .setPeriodStartDate(window.periodStartDate())
            .setPeriodEndDate(window.periodEndDate())
            .setSysOrigin(sysOrigin)
            .setCountryCode(countryCode)
            .setCountryName(countryName)
            .setGameProvider(gameProvider)
            .setGameId(gameId)
            .setGameName(gameName)
            .setConsumeAmount(BigDecimal.ZERO)
            .setPayoutAmount(BigDecimal.ZERO)
            .setProfitAmount(BigDecimal.ZERO)
            .setUserCount(0L)
            .setOrderCount(0L));
  }

  private CountryDashboardGameMetricCO toCO(CountryDashboardGameMetricDTO dto) {
    BigDecimal consume = safe(dto.getConsumeAmount());
    BigDecimal payout = safe(dto.getPayoutAmount());
    BigDecimal profit = safe(dto.getProfitAmount());
    return new CountryDashboardGameMetricCO()
        .setPeriodKey(dto.getPeriodKey())
        .setPeriodName(dto.getPeriodName())
        .setStatTimezone(dto.getStatTimezone())
        .setCountryCode(dto.getCountryCode())
        .setCountryName(dto.getCountryName())
        .setGameProvider(dto.getGameProvider())
        .setGameId(dto.getGameId())
        .setGameName(dto.getGameName())
        .setConsumeAmount(consume)
        .setPayoutAmount(payout)
        .setProfitAmount(profit)
        .setPayoutRate(percent(payout, consume))
        .setProfitRate(percent(profit, consume))
        .setUserCount(safeLong(dto.getUserCount()))
        .setOrderCount(safeLong(dto.getOrderCount()))
        .setRefreshedAt(dto.getRefreshedAt() == null ? null : dto.getRefreshedAt().format(DATETIME_FORMATTER));
  }

  private PeriodWindow toDayWindow(LocalDate statDate) {
    String key = statDate.format(DATE_FORMATTER);
    return new PeriodWindow(PERIOD_DAY, key, key, statDate, statDate.plusDays(1));
  }

  private PeriodWindow toWeekWindow(LocalDate statDate, ZoneId statZoneId) {
    LocalDate start = statDate.with(DayOfWeek.MONDAY);
    LocalDate end = capCurrentPeriodEnd(start.plusDays(7), statZoneId);
    int weekYear = statDate.get(WEEK_FIELDS.weekBasedYear());
    int week = statDate.get(WEEK_FIELDS.weekOfWeekBasedYear());
    String key = String.format(Locale.ROOT, "%d-W%02d", weekYear, week);
    return new PeriodWindow(PERIOD_WEEK, key, key, start, end);
  }

  private PeriodWindow toMonthWindow(LocalDate statDate, ZoneId statZoneId) {
    LocalDate start = statDate.withDayOfMonth(1);
    LocalDate end = capCurrentPeriodEnd(start.plusMonths(1), statZoneId);
    String key = statDate.format(MONTH_FORMATTER);
    return new PeriodWindow(PERIOD_MONTH, key, key, start, end);
  }

  private LocalDate capCurrentPeriodEnd(LocalDate naturalEndExclusive, ZoneId statZoneId) {
    LocalDate tomorrow = LocalDate.now(statZoneId).plusDays(1);
    return naturalEndExclusive.isAfter(tomorrow) ? tomorrow : naturalEndExclusive;
  }

  private TimeWindow toStorageTimeWindow(PeriodWindow window, StatTimezone statTimezone) {
    Instant startInstant = window.periodStartDate().atStartOfDay(statTimezone.zoneId()).toInstant();
    Instant endInstant = window.periodEndDate().atStartOfDay(statTimezone.zoneId()).toInstant();
    return new TimeWindow(
        LocalDateTime.ofInstant(startInstant, STORAGE_ZONE_ID),
        LocalDateTime.ofInstant(endInstant, STORAGE_ZONE_ID),
        startInstant.toEpochMilli(),
        endInstant.toEpochMilli());
  }

  private List<LocalDate> recentDates(int days, ZoneId zoneId) {
    LocalDate today = LocalDate.now(zoneId);
    List<LocalDate> statDates = new ArrayList<>();
    for (int i = days - 1; i >= 0; i--) {
      statDates.add(today.minusDays(i));
    }
    return statDates;
  }

  private List<StatTimezone> resolveStatTimezones(String statTimezonesValue) {
    String config = trimToNull(statTimezonesValue);
    if (config == null) {
      config = trimToNull(refreshTimezones);
    }
    if (config == null) {
      config = "Asia/Riyadh,UTC,Asia/Shanghai";
    }

    Map<String, StatTimezone> zones = new LinkedHashMap<>();
    Arrays.stream(config.split(","))
        .map(String::trim)
        .map(CountryDashboardGameMetricServiceImpl::normalizeStatTimezone)
        .filter(Objects::nonNull)
        .forEach(zone -> zones.putIfAbsent(zone.id(), zone));
    if (zones.isEmpty()) {
      zones.put(STORAGE_ZONE_ID.getId(), new StatTimezone(STORAGE_ZONE_ID.getId(), STORAGE_ZONE_ID,
          zoneOffset(STORAGE_ZONE_ID)));
    }
    return new ArrayList<>(zones.values());
  }

  private List<String> resolveSysOrigins() {
    String config = trimToNull(refreshSysOrigins);
    if (config == null) {
      return List.of(SysOriginPlatformEnum.LIKEI.name());
    }
    if ("ALL".equalsIgnoreCase(config)) {
      return SysOriginPlatformEnum.getVoiceSystems().stream()
          .map(SysOriginPlatformEnum::name)
          .filter(Objects::nonNull)
          .distinct()
          .toList();
    }
    List<String> sysOrigins = Arrays.stream(config.split(","))
        .map(String::trim)
        .filter(CountryDashboardGameMetricServiceImpl::hasText)
        .distinct()
        .toList();
    return sysOrigins.isEmpty() ? List.of(SysOriginPlatformEnum.LIKEI.name()) : sysOrigins;
  }

  private static SortSpec normalizeSort(String sortField, String sortOrder) {
    String column = switch (Objects.toString(sortField, "")) {
      case "payoutAmount" -> "t.payout_amount";
      case "profitAmount" -> "t.profit_amount";
      case "userCount" -> "t.user_count";
      case "orderCount" -> "t.order_count";
      case "consumeAmount" -> "t.consume_amount";
      default -> null;
    };
    String direction = "ascend".equalsIgnoreCase(sortOrder) ? "ASC" : "DESC";
    return new SortSpec(column, direction);
  }

  private static SortSpec normalizeSummarySort(String sortField, String sortOrder) {
    String column = switch (Objects.toString(sortField, "")) {
      case "payoutAmount" -> "payoutAmount";
      case "profitAmount" -> "profitAmount";
      case "userCount" -> "userCount";
      case "orderCount" -> "orderCount";
      case "consumeAmount" -> "consumeAmount";
      default -> null;
    };
    String direction = "ascend".equalsIgnoreCase(sortOrder) ? "ASC" : "DESC";
    return new SortSpec(column, direction);
  }

  private static String normalizeGameProvider(String gameProvider) {
    String value = trimToNull(gameProvider);
    return value == null ? null : value.toUpperCase(Locale.ROOT);
  }

  private static StatTimezone normalizeStatTimezone(String statTimezone) {
    String value = trimToNull(statTimezone);
    if (value == null) {
      return null;
    }
    String upperValue = value.toUpperCase(Locale.ROOT);
    ZoneId zoneId = switch (upperValue) {
      case "UTC", "Z", "+00:00" -> ZoneId.of("UTC");
      case "ASIA/SHANGHAI", "BEIJING", "CHINA", "+08:00" -> ZoneId.of("Asia/Shanghai");
      case "ASIA/RIYADH", "RIYADH", "SERVER", "+03:00" -> STORAGE_ZONE_ID;
      default -> null;
    };
    if (zoneId == null) {
      return null;
    }
    return new StatTimezone(zoneId.getId(), zoneId, zoneOffset(zoneId));
  }

  private static String normalizeCsv(String value) {
    if (!hasText(value)) {
      return null;
    }
    String normalized = Arrays.stream(value.split(","))
        .map(String::trim)
        .filter(CountryDashboardGameMetricServiceImpl::hasText)
        .distinct()
        .reduce((left, right) -> left + "," + right)
        .orElse(null);
    return hasText(normalized) ? normalized : null;
  }

  private static BigDecimal add(BigDecimal left, BigDecimal right) {
    return safe(left).add(safe(right));
  }

  private static BigDecimal safe(BigDecimal value) {
    return value == null ? BigDecimal.ZERO : value;
  }

  private static BigDecimal percent(BigDecimal numerator, BigDecimal denominator) {
    BigDecimal safeDenominator = safe(denominator);
    if (BigDecimal.ZERO.compareTo(safeDenominator) == 0) {
      return BigDecimal.ZERO;
    }
    return safe(numerator)
        .multiply(BigDecimal.valueOf(100))
        .divide(safeDenominator, 4, RoundingMode.HALF_UP);
  }

  private static long safeLong(Long value) {
    return value == null ? 0L : value;
  }

  private static boolean hasText(String value) {
    return value != null && !value.trim().isEmpty();
  }

  private static String trimToNull(String value) {
    if (value == null) {
      return null;
    }
    String trimmed = value.trim();
    return trimmed.isEmpty() ? null : trimmed;
  }

  private static String zoneOffset(ZoneId zoneId) {
    ZoneOffset offset = zoneId.getRules().getOffset(Instant.now());
    String id = offset.getId();
    return "Z".equals(id) ? "+00:00" : id;
  }

  private record PeriodWindow(String periodType, String periodKey, String periodName,
      LocalDate periodStartDate, LocalDate periodEndDate) {
  }

  private record StatTimezone(String id, ZoneId zoneId, String offset) {
  }

  private record TimeWindow(LocalDateTime startTime, LocalDateTime endTime,
      Long startTimeMillis, Long endTimeMillis) {
  }

  private record RefreshWriteResult(int deleted, int inserted, int userRows) {
  }

  private record SortSpec(String column, String direction) {
  }

  private static final class QueryCondition {

    private final String periodType;
    private final String statTimezone;
    private final ZoneId statZoneId;
    private final LocalDate startDateValue;
    private final LocalDate endDateValue;
    private final LocalDateTime startTime;
    private final LocalDateTime endTime;
    private final String startPeriodKey;
    private final String endPeriodKey;
    private final String countryKeyword;
    private final String sysOrigin;

    private QueryCondition(String periodType, String statTimezone, ZoneId statZoneId,
        LocalDate startDateValue, LocalDate endDateValue, LocalDateTime startTime,
        LocalDateTime endTime, String startPeriodKey, String endPeriodKey,
        String countryKeyword, String sysOrigin) {
      this.periodType = periodType;
      this.statTimezone = statTimezone;
      this.statZoneId = statZoneId;
      this.startDateValue = startDateValue;
      this.endDateValue = endDateValue;
      this.startTime = startTime;
      this.endTime = endTime;
      this.startPeriodKey = startPeriodKey;
      this.endPeriodKey = endPeriodKey;
      this.countryKeyword = countryKeyword;
      this.sysOrigin = sysOrigin;
    }

    private static QueryCondition of(CountryDashboardGameMetricQryCmd cmd) {
      String periodType = normalizePeriodType(cmd == null ? null : cmd.getPeriodType());
      ZoneId statZoneId = normalizeStatZoneId(cmd == null ? null : cmd.getStatTimezone());
      LocalDate start = parseDate(cmd == null ? null : cmd.getStartDate());
      LocalDate end = parseDate(cmd == null ? null : cmd.getEndDate());
      if (!PERIOD_ALL.equals(periodType) && start == null && end == null) {
        LocalDate today = LocalDate.now(statZoneId);
        switch (periodType) {
          case PERIOD_WEEK -> {
            start = today.with(DayOfWeek.MONDAY);
            end = today.with(DayOfWeek.SUNDAY);
          }
          case PERIOD_MONTH -> {
            start = today.withDayOfMonth(1);
            end = today.withDayOfMonth(today.lengthOfMonth());
          }
          default -> {
            start = today;
            end = today;
          }
        }
      }
      if (start != null && end == null) {
        end = start;
      }
      if (start == null && end != null) {
        start = end;
      }
      if (start != null && end != null && end.isBefore(start)) {
        throw new IllegalArgumentException("endDate must not be before startDate.");
      }
      Instant startInstant = start == null ? null : start.atStartOfDay(statZoneId).toInstant();
      Instant endInstant = end == null ? null : end.plusDays(1).atStartOfDay(statZoneId).toInstant();
      LocalDateTime storageStartTime = startInstant == null ? null
          : LocalDateTime.ofInstant(startInstant, STORAGE_ZONE_ID);
      LocalDateTime storageEndTime = endInstant == null ? null
          : LocalDateTime.ofInstant(endInstant, STORAGE_ZONE_ID);
      String startPeriodKey = PERIOD_DAY.equals(periodType) && start != null
          ? start.format(DATE_FORMATTER) : null;
      String endPeriodKey = PERIOD_DAY.equals(periodType) && end != null
          ? end.format(DATE_FORMATTER) : null;
      return new QueryCondition(
          periodType,
          statZoneId.getId(),
          statZoneId,
          start,
          end,
          storageStartTime,
          storageEndTime,
          startPeriodKey,
          endPeriodKey,
          trimToNull(cmd == null ? null : cmd.getCountryKeyword()),
          trimToNull(cmd == null ? null : cmd.getSysOrigin())
      );
    }

    private static String normalizePeriodType(String periodType) {
      String value = trimToNull(periodType);
      if (value == null) {
        return PERIOD_DAY;
      }
      value = value.toUpperCase(Locale.ROOT);
      return switch (value) {
        case PERIOD_DAY, PERIOD_WEEK, PERIOD_MONTH, PERIOD_ALL -> value;
        default -> PERIOD_DAY;
      };
    }

    private static ZoneId normalizeStatZoneId(String statTimezone) {
      StatTimezone normalized = normalizeStatTimezone(statTimezone);
      return normalized == null ? STORAGE_ZONE_ID : normalized.zoneId();
    }

    private static LocalDate parseDate(String value) {
      String fixed = trimToNull(value);
      return fixed == null ? null : LocalDate.parse(fixed, DATE_FORMATTER);
    }
  }
}
