package shop.alien.store.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import shop.alien.entity.result.R;
import shop.alien.entity.store.CommonPushReview;
import shop.alien.entity.store.CommonPushTask;
import shop.alien.entity.store.CommonPushTaskStatus;
import shop.alien.entity.store.dto.CommonPushTaskStatsDto;
import shop.alien.entity.store.vo.*;
import shop.alien.mapper.CommonPushTaskMapper;
import shop.alien.store.dto.CommonPushSendResultDto;
import shop.alien.store.dto.CommonPushTestSendDto;
import shop.alien.store.service.CommonPushReviewService;
import shop.alien.store.service.CommonPushSendService;
import shop.alien.store.service.CommonPushTaskService;
import shop.alien.store.service.CommonPushTaskStatsService;
import shop.alien.store.util.CommonPushVendorTaskIdUtil;
import shop.alien.store.util.ai.AiContentModerationUtil;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class CommonPushTaskServiceImpl extends ServiceImpl<CommonPushTaskMapper, CommonPushTask> implements CommonPushTaskService {

    private static final String[] WEEKDAY_NAMES = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};

    /** 审核表状态：审核通过 */
    private static final int REVIEW_STATUS_APPROVED = 2;
    /** 审核表状态：审核驳回 */
    private static final int REVIEW_STATUS_REJECTED = 3;

    private final CommonPushSendService commonPushSendService;
    private final CommonPushReviewService commonPushReviewService;
    private final AiContentModerationUtil aiContentModerationUtil;
    private final CommonPushTaskStatsService commonPushTaskStatsService;

    /**
     * 新增推送任务：校验 → 填默认值 → AI 审核 → 入库 → 按 sendType 决定是否立即下发。
     * <p>sendType=1 立即发送时调用 {@link #doSendTask}；sendType=2 仅入库等待定时任务。</p>
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R<String> add(CommonPushTask task) {
        log.info("CommonPushTaskServiceImpl.add, param={}", task);
        R<String> validated = validateTaskForAdd(task);
        if (validated != null) {
            return validated;
        }
        fillTaskDefaults(task);
        task.setStatus(CommonPushTaskStatus.PENDING_REVIEW);

        String rejectReason = auditPushContent(task);
        boolean auditPassed = rejectReason == null;
        task.setStatus(auditPassed ? CommonPushTaskStatus.REVIEW_PASSED : CommonPushTaskStatus.REJECTED);

        if (!this.save(task)) {
            return R.fail("新增失败");
        }
        savePushReview(task.getId(), auditPassed, rejectReason);

        if (!auditPassed) {
            return R.fail("审核未通过：" + rejectReason);
        }

        if (Integer.valueOf(1).equals(task.getSendType())) {
            R<String> sendResult = doSendTask(task);
            return sendResult;
        }
        if (Integer.valueOf(2).equals(task.getSendType())) {
            return R.success("新增成功，任务将于 " + formatDateTime(task.getScheduledAt()) + " 发送");
        }
        return R.success("新增成功");
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public R<Long> saveDraft(CommonPushTask task) {
        log.info("CommonPushTaskServiceImpl.saveDraft, param={}", task);
        if (task == null) {
            return R.fail("任务不能为空");
        }
        fillTaskDefaults(task);
        task.setStatus(CommonPushTaskStatus.DRAFT);

        if (task.getId() == null) {
            if (!this.save(task)) {
                return R.fail("保存草稿失败");
            }
            return R.data(task.getId(), "保存草稿成功");
        }

        CommonPushTask existing = this.getById(task.getId());
        if (existing == null) {
            return R.fail("草稿不存在");
        }
        if (!Integer.valueOf(CommonPushTaskStatus.DRAFT).equals(existing.getStatus())) {
            return R.fail("仅草稿状态的任务可更新草稿");
        }
        task.setStatus(CommonPushTaskStatus.DRAFT);
        if (!this.updateById(task)) {
            return R.fail("保存草稿失败");
        }
        return R.data(task.getId(), "保存草稿成功");
    }

    @Override
    public int executeScheduledPushTasks() {
        Date now = new Date();
        markExpiredScheduledTasks(now);

        List<CommonPushTask> dueTasks = this.list(new LambdaQueryWrapper<CommonPushTask>()
                .eq(CommonPushTask::getSendType, 2)
                .eq(CommonPushTask::getStatus, CommonPushTaskStatus.REVIEW_PASSED)
                .le(CommonPushTask::getScheduledAt, now)
                .and(w -> w.isNull(CommonPushTask::getExpireAt).or().gt(CommonPushTask::getExpireAt, now))
                .orderByAsc(CommonPushTask::getScheduledAt)
                .orderByAsc(CommonPushTask::getId));

        if (dueTasks == null || dueTasks.isEmpty()) {
            return 0;
        }

        int successCount = 0;
        for (CommonPushTask task : dueTasks) {
            try {
                R<String> result = doSendTask(task);
                if (R.isSuccess(result)) {
                    successCount++;
                }
            } catch (Exception e) {
                log.error("定时推送任务执行异常, taskId={}, taskNo={}", task.getId(), task.getTaskNo(), e);
            }
        }
        return successCount;
    }

    @Override
    public int syncPushTaskStatistics() {
        return commonPushTaskStatsService.syncPushTaskStatistics();
    }

    private void markExpiredScheduledTasks(Date now) {
        List<CommonPushTask> expiredTasks = this.list(new LambdaQueryWrapper<CommonPushTask>()
                .eq(CommonPushTask::getSendType, 2)
                .eq(CommonPushTask::getStatus, CommonPushTaskStatus.REVIEW_PASSED)
                .le(CommonPushTask::getScheduledAt, now)
                .isNotNull(CommonPushTask::getExpireAt)
                .lt(CommonPushTask::getExpireAt, now));
        if (expiredTasks == null || expiredTasks.isEmpty()) {
            return;
        }
        for (CommonPushTask task : expiredTasks) {
            CommonPushTask update = new CommonPushTask();
            update.setId(task.getId());
            update.setStatus(CommonPushTaskStatus.FAILED);
            this.updateById(update);
            log.warn("定时推送任务已过期未发送, taskId={}, taskNo={}, expireAt={}",
                    task.getId(), task.getTaskNo(), task.getExpireAt());
        }
    }

    /**
     * 执行推送并回写任务状态。
     * <p>成功：status=2(已发送)，回填 actualCount、vendorTaskIds；失败：status=4。</p>
     * <p>格式转换：vendorTaskIds Map → 字符串存入 common_push_task.vendor_task_ids</p>
     * <pre>
     * 转换前 Map: { "huawei_task_id": "178169515953238426090801" }
     * 转换后 String: "huawei_task_id: 178169515953238426090801"
     * </pre>
     */
    private R<String> doSendTask(CommonPushTask task) {
        CommonPushSendResultDto sendResult;
        try {
            sendResult = commonPushSendService.send(task);
        } catch (Exception e) {
            log.error("推送任务执行异常, taskId={}, taskNo={}", task.getId(), task.getTaskNo(), e);
            task.setStatus(CommonPushTaskStatus.FAILED);
            this.updateById(task);
            return R.fail("推送失败：" + e.getMessage());
        }
        if (!sendResult.isSuccess()) {
            task.setStatus(CommonPushTaskStatus.FAILED);
            this.updateById(task);
            return R.fail(sendResult.getMessage());
        }
        task.setActualCount(sendResult.getSentCount());
        task.setStatus(CommonPushTaskStatus.SENT);
        task.setVendorTaskIds(CommonPushVendorTaskIdUtil.format(sendResult.getVendorTaskIds()));
        this.updateById(task);
        commonPushTaskStatsService.initTaskNumRecords(task.getId(), sendResult.getVendorTaskIds(), task.getEstimatedCount());
        commonPushSendService.saveTaskUserRecords(task.getId(), sendResult);
        return R.success(sendResult.getMessage());
    }

    /** 将 Date 格式化为界面展示用字符串 yyyy-MM-dd HH:mm:ss（定时任务提示文案）。 */
    private String formatDateTime(Date date) {
        if (date == null) {
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
    }

    @Override
    public R<CommonPushSendResultDto> sendTest(CommonPushTestSendDto dto) {
        log.info("CommonPushTaskServiceImpl.sendTest, param={}", dto);
        if (dto == null) {
            return R.fail("请求参数不能为空");
        }
        CommonPushSendResultDto result = commonPushSendService.sendTest(
                dto.getDeviceId(),
                dto.getTitle(),
                dto.getContent(),
                dto.getVendorChannel(),
                dto.getJumpUrl(),
                dto.getPlatform());
        return result.isSuccess() ? R.data(result, result.getMessage()) : R.fail(result.getMessage());
    }

    /**
     * 新增任务参数校验。
     *
     * @return null 表示通过；非 null 为 R.fail 可直接返回
     */
    private R<String> validateTaskForAdd(CommonPushTask task) {
        if (task == null) {
            return R.fail("任务不能为空");
        }
        if (StringUtils.isBlank(task.getTitle())) {
            return R.fail("推送标题不能为空");
        }
        if (StringUtils.isBlank(task.getContent())) {
            return R.fail("推送内容不能为空");
        }
        if (task.getSendType() == null) {
            return R.fail("发送方式不能为空");
        }
        if (Integer.valueOf(1).equals(task.getSendType()) && task.getTargetType() == null) {
            return R.fail("立即发送时目标类型不能为空");
        }
        if (Integer.valueOf(2).equals(task.getSendType())) {
            if (task.getTargetType() == null) {
                return R.fail("定时发送时目标类型不能为空");
            }
            if (task.getScheduledAt() == null) {
                return R.fail("定时发送时执行时间不能为空");
            }
            if (!task.getScheduledAt().after(new Date())) {
                return R.fail("执行时间必须晚于当前时间");
            }
            if (task.getExpireAt() != null && !task.getExpireAt().after(task.getScheduledAt())) {
                return R.fail("过期时间必须晚于执行时间");
            }
        }
        return null;
    }

    /**
     * AI 审核推送标题与正文（敏感词/违规内容）。
     *
     * @return 驳回原因；通过时返回 null
     */
    private String auditPushContent(CommonPushTask task) {
        AiContentModerationUtil.AuditResult titleAudit =
                aiContentModerationUtil.auditContent(task.getTitle(), null);
        if (!titleAudit.isPassed()) {
            log.warn("推送标题审核未通过: {}", titleAudit.getFailureReason());
            return "推送标题：" + titleAudit.getFailureReason();
        }
        AiContentModerationUtil.AuditResult contentAudit =
                aiContentModerationUtil.auditContent(task.getContent(), null);
        if (!contentAudit.isPassed()) {
            log.warn("推送内容审核未通过: {}", contentAudit.getFailureReason());
            return "推送内容：" + contentAudit.getFailureReason();
        }
        return null;
    }

    /** 写入 common_push_review 审核记录（通过/驳回及原因）。 */
    private void savePushReview(Long pushTaskId, boolean auditPassed, String rejectReason) {
        CommonPushReview review = new CommonPushReview();
        review.setPushTaskId(pushTaskId);
        review.setReviewAt(new Date());
        if (auditPassed) {
            review.setReviewStatus(REVIEW_STATUS_APPROVED);
            review.setRejectCount(0);
        } else {
            review.setReviewStatus(REVIEW_STATUS_REJECTED);
            review.setRejectReason(rejectReason);
            review.setRejectCount(1);
        }
        commonPushReviewService.save(review);
    }

    /**
     * 填充任务默认值（未传字段时）。
     * <p>格式转换 taskNo：</p>
     * <pre>
     * 转换前: taskNo = null
     * 转换后: taskNo = "TS202606171916012942"  （TS + yyyyMMddHHmmss + 4位随机数）
     * </pre>
     */
    private void fillTaskDefaults(CommonPushTask task) {
        if (StringUtils.isBlank(task.getTaskNo())) {
            task.setTaskNo("TS" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
                    + String.format("%04d", (int) (Math.random() * 10000)));
        }
        if (task.getEstimatedCount() == null) {
            task.setEstimatedCount(0);
        }
        if (task.getActualCount() == null) {
            task.setActualCount(0);
        }
    }

    @Override
    public R<String> deleteById(Long id) {
        log.info("CommonPushTaskServiceImpl.deleteById, id={}", id);
        boolean result = this.removeById(id);
        return result ? R.success("删除成功") : R.fail("删除失败");
    }

    @Override
    public R<String> update(CommonPushTask task) {
        log.info("CommonPushTaskServiceImpl.update, param={}", task);
        if (task.getId() == null) {
            return R.fail("id不能为空");
        }
        boolean result = this.updateById(task);
        return result ? R.success("更新成功") : R.fail("更新失败");
    }

    @Override
    public R<CommonPushTask> getInfoById(Long id) {
        log.info("CommonPushTaskServiceImpl.getInfoById, id={}", id);
        return R.data(this.getById(id));
    }

    @Override
    public R<IPage<CommonPushTask>> list(Integer pageNum, Integer pageSize, String taskNo, String title, String status, Integer pushType) {
        log.info("CommonPushTaskServiceImpl.list, pageNum={}, pageSize={}, taskNo={}, title={}, status={}, pushType={}",
                pageNum, pageSize, taskNo, title, status, pushType);
        Page<CommonPushTask> page = new Page<>(pageNum, pageSize);
        LambdaQueryWrapper<CommonPushTask> wrapper = new LambdaQueryWrapper<>();
        if (StringUtils.isNotBlank(taskNo)) {
            wrapper.eq(CommonPushTask::getTaskNo, taskNo);
        }
        if (StringUtils.isNotBlank(title)) {
            wrapper.like(CommonPushTask::getTitle, title);
        }
        if (StringUtils.isNotBlank(status)) {
            wrapper.eq(CommonPushTask::getStatus, status);
        }
        if (pushType != null) {
            wrapper.eq(CommonPushTask::getPushType, pushType);
        }
        wrapper.orderByDesc(CommonPushTask::getUpdatedTime);
        return R.data(this.page(page, wrapper));
    }

    @Override
    public R<CommonPushTaskStatisticsPageVo> statisticsList(Integer pageNum, Integer pageSize, String taskNo, String keyword,
                                                            String status, Integer pushType, String channel,
                                                            Date startTime, Date endTime) {
        log.info("CommonPushTaskServiceImpl.statisticsList, pageNum={}, pageSize={}, taskNo={}, keyword={}, status={}, pushType={}, channel={}, startTime={}, endTime={}",
                pageNum, pageSize, taskNo, keyword, status, pushType, channel, startTime, endTime);
        Date queryStartTime = normalizeQueryStartTime(startTime);
        Date queryEndTime = normalizeQueryEndTime(endTime);
        Page<CommonPushTaskStatsDto> page = new Page<>(pageNum, pageSize);
        IPage<CommonPushTaskStatsDto> statsPage = baseMapper.selectStatisticsPage(page, taskNo, keyword, status, pushType, channel, queryStartTime, queryEndTime);

        CommonPushTaskStatsDto summaryStats = baseMapper.selectStatisticsSummary(taskNo, keyword, status, pushType, channel, queryStartTime, queryEndTime);

        CommonPushTaskStatisticsPageVo result = new CommonPushTaskStatisticsPageVo();
        result.setSummary(buildUserStatsSummary(summaryStats != null ? summaryStats : emptyUserStats()));
        result.setPage(statsPage.convert(this::toStatisticsItem));
        log.info("CommonPushTaskServiceImpl.statisticsList, result={}", result);
        return R.data(result);
    }

    @Override
    public R<CommonPushFunnelVo> funnel(String phoneType, Date startTime, Date endTime) {
        log.info("CommonPushTaskServiceImpl.funnel, phoneType={}, startTime={}, endTime={}", phoneType, startTime, endTime);
        CommonPushTaskStatsDto stats = baseMapper.selectUserStats(startTime, endTime, phoneType);
        if (stats == null) {
            stats = emptyUserStats();
        }

        CommonPushFunnelVo funnelVo = new CommonPushFunnelVo();
        funnelVo.setPhoneType(phoneType);
        funnelVo.setSteps(buildFunnelSteps(stats));
        log.info("CommonPushTaskServiceImpl.funnel, result={}", funnelVo);
        return R.data(funnelVo);
    }

    @Override
    public R<CommonPushReportVo> report(Integer reportType) {
        log.info("CommonPushTaskServiceImpl.report, reportType={}", reportType);
        if (reportType == null) {
            reportType = 1;
        }
        Date reportDate = truncateToDay(new Date());

        Date[] range = resolveReportRange(reportType, reportDate);
        Date startTime = range[0];
        Date endTime = range[1];

        CommonPushTaskStatsDto stats = baseMapper.selectUserStats(startTime, endTime, null);
        if (stats == null) {
            stats = emptyUserStats();
        }

        CommonPushReportVo reportVo = new CommonPushReportVo();
        reportVo.setReportType(reportType);
        reportVo.setReportDate(reportDate);
        reportVo.setStartTime(startTime);
        reportVo.setEndTime(endTime);
        reportVo.setReportTitle(buildReportTitle(reportType, reportDate));
        reportVo.setSummary(buildUserStatsSummary(stats));
        reportVo.setTopList(buildTopList(startTime, endTime));
        log.info("CommonPushTaskServiceImpl.report, result={}", reportVo);
        return R.data(reportVo);
    }

    private CommonPushTaskStatisticsItemVo toStatisticsItem(CommonPushTaskStatsDto dto) {
        CommonPushTaskStatisticsItemVo item = new CommonPushTaskStatisticsItemVo();
        item.setId(dto.getId());
        item.setTaskNo(dto.getTaskNo());
        item.setTitle(dto.getTitle());
        item.setPushType(dto.getPushType());
        item.setStatus(dto.getStatus());
        item.setChannels(dto.getChannels());
        item.setEstimatedCount(dto.getEstimatedCount());
        item.setCreatedTime(dto.getCreatedTime());
        item.setSentCount(defaultCount(dto.getSentCount()));
        item.setDeliveredCount(defaultCount(dto.getDeliveredCount()));
        item.setClickCount(defaultCount(dto.getClickCount()));
        item.setPushRate(calcRate(item.getSentCount(), dto.getEstimatedCount() != null ? dto.getEstimatedCount().longValue() : 0L));
        item.setDeliveryRate(calcRate(item.getDeliveredCount(), item.getSentCount()));
        item.setClickRate(calcRate(item.getClickCount(), item.getDeliveredCount()));
        return item;
    }

    private CommonPushStatisticsSummaryVo buildUserStatsSummary(CommonPushTaskStatsDto stats) {
        return buildSummary(defaultCount(stats.getSentCount()), defaultCount(stats.getDeliveredCount()), defaultCount(stats.getClickCount()));
    }

    private CommonPushStatisticsSummaryVo buildSummary(long sentCount, long deliveredCount, long clickCount) {
        CommonPushStatisticsSummaryVo summary = new CommonPushStatisticsSummaryVo();
        summary.setPushVolume(sentCount);
        summary.setDeliveredCount(deliveredCount);
        summary.setClickCount(clickCount);
        summary.setDeliveryRate(calcRate(deliveredCount, sentCount));
        summary.setClickRate(calcRate(clickCount, deliveredCount));
        summary.setConversionRate(calcRate(clickCount, sentCount));
        return summary;
    }

    private List<CommonPushFunnelStepVo> buildFunnelSteps(CommonPushTaskStatsDto stats) {
        long actualSend = defaultCount(stats.getSentCount());
        long arrived = defaultCount(stats.getDeliveredCount());
        long show = defaultCount(stats.getShowCount());
        long click = defaultCount(stats.getClickCount());

        List<CommonPushFunnelStepVo> steps = new ArrayList<>();

        CommonPushFunnelStepVo actual = new CommonPushFunnelStepVo();
        actual.setStepCode("actual");
        actual.setStepName("实际发送");
        actual.setCount(actualSend);
        actual.setConversionRate(BigDecimal.valueOf(100));
        steps.add(actual);

        CommonPushFunnelStepVo arrivedStep = new CommonPushFunnelStepVo();
        arrivedStep.setStepCode("arrived");
        arrivedStep.setStepName("抵达设备");
        arrivedStep.setCount(arrived);
        arrivedStep.setConversionRate(calcRate(arrived, actualSend));
        steps.add(arrivedStep);

        CommonPushFunnelStepVo showStep = new CommonPushFunnelStepVo();
        showStep.setStepCode("show");
        showStep.setStepName("展示通知");
        showStep.setCount(show);
        showStep.setConversionRate(calcRate(show, arrived));
        steps.add(showStep);

        CommonPushFunnelStepVo clickStep = new CommonPushFunnelStepVo();
        clickStep.setStepCode("click");
        clickStep.setStepName("用户点击");
        clickStep.setCount(click);
        clickStep.setConversionRate(calcRate(click, arrived));
        steps.add(clickStep);

        return steps;
    }

    private CommonPushTaskStatsDto emptyUserStats() {
        CommonPushTaskStatsDto stats = new CommonPushTaskStatsDto();
        stats.setSentCount(0L);
        stats.setDeliveredCount(0L);
        stats.setShowCount(0L);
        stats.setClickCount(0L);
        return stats;
    }

    private List<CommonPushReportTopItemVo> buildTopList(Date startTime, Date endTime) {
        List<CommonPushTaskStatsDto> topStats = baseMapper.selectTopByClickRate(startTime, endTime, 5);
        List<CommonPushReportTopItemVo> topList = new ArrayList<>();
        if (topStats == null) {
            return topList;
        }
        int rank = 1;
        for (CommonPushTaskStatsDto dto : topStats) {
            CommonPushReportTopItemVo item = new CommonPushReportTopItemVo();
            item.setRank(rank++);
            item.setPushTaskId(dto.getId());
            item.setTitle(dto.getTitle());
            item.setDeliveredCount(defaultCount(dto.getDeliveredCount()));
            item.setClickCount(defaultCount(dto.getClickCount()));
            item.setClickRate(calcRate(item.getClickCount(), item.getDeliveredCount()));
            topList.add(item);
        }
        return topList;
    }

    private String buildReportTitle(Integer reportType, Date reportDate) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String dateStr = dateFormat.format(reportDate);
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(reportDate);
        String weekday = WEEKDAY_NAMES[calendar.get(Calendar.DAY_OF_WEEK) - 1];
        if (reportType == 2) {
            return "推送周报 — " + dateStr;
        }
        if (reportType == 3) {
            SimpleDateFormat monthFormat = new SimpleDateFormat("yyyy-MM");
            return "推送月报 — " + monthFormat.format(reportDate);
        }
        return "推送日报 — " + dateStr + " (" + weekday + ")";
    }

    private Date[] resolveReportRange(Integer reportType, Date reportDate) {
        Calendar start = Calendar.getInstance();
        start.setTime(reportDate);
        start.set(Calendar.HOUR_OF_DAY, 0);
        start.set(Calendar.MINUTE, 0);
        start.set(Calendar.SECOND, 0);
        start.set(Calendar.MILLISECOND, 0);

        Calendar end = (Calendar) start.clone();
        if (reportType == 2) {
            int dayOfWeek = start.get(Calendar.DAY_OF_WEEK);
            int diffToMonday = (dayOfWeek + 5) % 7;
            start.add(Calendar.DAY_OF_MONTH, -diffToMonday);
            end.setTime(start.getTime());
            end.add(Calendar.DAY_OF_MONTH, 7);
        } else if (reportType == 3) {
            start.set(Calendar.DAY_OF_MONTH, 1);
            end.setTime(start.getTime());
            end.add(Calendar.MONTH, 1);
        } else {
            end.add(Calendar.DAY_OF_MONTH, 1);
        }
        return new Date[]{start.getTime(), end.getTime()};
    }

    private Date truncateToDay(Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        return calendar.getTime();
    }

    /** 查询开始时间归一化到当天 00:00:00 */
    private Date normalizeQueryStartTime(Date startTime) {
        return startTime == null ? null : truncateToDay(startTime);
    }

    /** 查询结束时间归一化到当天 23:59:59.999，传日期时可包含当天全天数据 */
    private Date normalizeQueryEndTime(Date endTime) {
        if (endTime == null) {
            return null;
        }
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(endTime);
        calendar.set(Calendar.HOUR_OF_DAY, 23);
        calendar.set(Calendar.MINUTE, 59);
        calendar.set(Calendar.SECOND, 59);
        calendar.set(Calendar.MILLISECOND, 999);
        return calendar.getTime();
    }

    private long defaultCount(Long count) {
        return count == null ? 0L : count;
    }

    private BigDecimal calcRate(long numerator, long denominator) {
        if (denominator <= 0) {
            return BigDecimal.ZERO;
        }
        return BigDecimal.valueOf(numerator)
                .multiply(BigDecimal.valueOf(100))
                .divide(BigDecimal.valueOf(denominator), 1, RoundingMode.HALF_UP);
    }
}
