package tech.easyflow.admin.service.dashboard.impl;

import cn.idev.excel.EasyExcel;
import cn.idev.excel.write.style.column.SimpleColumnWidthStyleStrategy;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.core.row.Db;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.service.BotService;
import tech.easyflow.admin.model.dashboard.DashboardChatStatusVo;
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendPointVo;
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendSeriesVo;
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
import tech.easyflow.admin.model.dashboard.DashboardOverviewVo;
import tech.easyflow.admin.model.dashboard.DashboardSummaryVo;
import tech.easyflow.admin.model.dashboard.DashboardTrendItemVo;
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
import tech.easyflow.admin.service.dashboard.DashboardService;
import tech.easyflow.chatlog.domain.dto.ChatActiveUserRank;
import tech.easyflow.chatlog.domain.dto.ChatAssistantSessionTrend;
import tech.easyflow.chatlog.domain.dto.ChatAssistantUsageRank;
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
import tech.easyflow.chatlog.domain.dto.ChatDashboardTrend;
import tech.easyflow.chatlog.service.ChatDashboardQueryService;
import tech.easyflow.common.constant.Constants;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysAccountRole;
import tech.easyflow.system.entity.SysRole;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.system.service.SysAccountRoleService;
import tech.easyflow.system.service.SysRoleService;

import javax.annotation.Resource;
import java.io.OutputStream;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 工作台统计服务实现。
 */
@Service
public class DashboardServiceImpl implements DashboardService {

    private static final Logger log = LoggerFactory.getLogger(DashboardServiceImpl.class);
    private static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault();
    private static final String CHAT_UNAVAILABLE_MESSAGE = "聊天数据不可用";
    private static final int DEFAULT_ASSISTANT_RANK_LIMIT = 5;
    private static final int DEFAULT_ASSISTANT_TREND_LIMIT = 8;
    private static final int DEFAULT_USER_RANK_LIMIT = 5;
    private static final int USER_RANK_EXPORT_COLUMN_WIDTH = 20;

    @Resource
    private SysAccountRoleService sysAccountRoleService;

    @Resource
    private SysRoleService sysRoleService;

    @Resource
    private SysAccountService sysAccountService;

    @Resource
    private ChatDashboardQueryService chatDashboardQueryService;

    @Resource
    private BotService botService;

    @Resource
    private CategoryPermissionService categoryPermissionService;

    /**
     * 获取工作台总览信息。
     *
     * @param loginAccount 当前登录账号
     * @param query 查询条件
     * @return 工作台总览
     */
    @Override
    public DashboardOverviewVo getOverview(LoginAccount loginAccount, DashboardOverviewQuery query) {
        DashboardQueryContext context = buildContext(loginAccount, query);
        DashboardSummaryVo summary = buildSummary(context);
        ChatDashboardPayload chatPayload = buildChatPayload(context, summary);

        DashboardOverviewVo result = new DashboardOverviewVo();
        result.setSummary(summary);
        result.setChatStatus(chatPayload.chatStatus);
        result.setTrends(chatPayload.trends);
        result.setAssistantTrends(chatPayload.assistantTrends);
        result.setDistribution(chatPayload.distribution);

        DashboardOverviewQuery normalizedQuery = new DashboardOverviewQuery();
        normalizedQuery.setRange(context.range);
        normalizedQuery.setStartDate(context.queryStartDate);
        normalizedQuery.setEndDate(context.queryEndDate);
        result.setQuery(normalizedQuery);
        result.setUpdatedAt(new Date());
        return result;
    }

    /**
     * 获取用户活跃榜。
     *
     * @param loginAccount 当前登录账号
     * @param query 查询条件
     * @return 用户活跃榜
     */
    @Override
    public List<DashboardUserRankItemVo> getUserRanks(LoginAccount loginAccount, DashboardUserRankQuery query) {
        DashboardQueryContext context = buildContext(loginAccount, query);
        return queryUserRanks(context, DEFAULT_USER_RANK_LIMIT);
    }

    /**
     * 导出用户活跃榜。
     *
     * @param loginAccount 当前登录账号
     * @param query 查询条件
     * @param outputStream 输出流
     */
    @Override
    public void exportUserRanks(LoginAccount loginAccount, DashboardUserRankQuery query, OutputStream outputStream) {
        DashboardQueryContext context = buildContext(loginAccount, query);
        List<DashboardUserRankItemVo> userRanks = queryUserRanks(context, null);
        EasyExcel.write(outputStream)
                .head(buildUserRankExportHead())
                .registerWriteHandler(new SimpleColumnWidthStyleStrategy(USER_RANK_EXPORT_COLUMN_WIDTH))
                .sheet("用户活跃榜")
                .doWrite(buildUserRankExportRows(userRanks));
    }

    /**
     * 构建顶部汇总卡片。
     *
     * @param context 查询上下文
     * @return 汇总结果
     */
    private DashboardSummaryVo buildSummary(DashboardQueryContext context) {
        DashboardSummaryVo summary = new DashboardSummaryVo();
        summary.setUserTotal(countScopedTable("tb_sys_account", "a", true, context));
        summary.setActiveUserTotal(countActiveUsers(context));
        summary.setBotTotal(countScopedTable("tb_bot", "b", false, context));
        summary.setWorkflowTotal(countScopedTable("tb_workflow", "w", false, context));
        summary.setKnowledgeBaseTotal(countScopedTable("tb_document_collection", "d", false, context));
        summary.setChatMessageTotal(0L);
        summary.setChatSessionTotal(0L);
        summary.setActiveAssistantTotal(0L);
        summary.setChatActiveUserTotal(0L);
        return summary;
    }

    /**
     * 构建聊天统计载荷。
     *
     * @param context 查询上下文
     * @param summary 汇总结果
     * @return 聊天统计载荷
     */
    private ChatDashboardPayload buildChatPayload(DashboardQueryContext context, DashboardSummaryVo summary) {
        DashboardChatStatusVo chatStatus = new DashboardChatStatusVo();
        chatStatus.setAvailable(Boolean.TRUE);
        chatStatus.setMessage("");

        if (!chatDashboardQueryService.available()) {
            chatStatus.setAvailable(Boolean.FALSE);
            chatStatus.setMessage(CHAT_UNAVAILABLE_MESSAGE);
            summary.setChatMessageTotal(0L);
            summary.setChatSessionTotal(0L);
            summary.setActiveAssistantTotal(0L);
            summary.setChatActiveUserTotal(0L);
            return new ChatDashboardPayload(
                    chatStatus,
                    new ArrayList<>(),
                    new ArrayList<>(),
                    new ArrayList<>()
            );
        }

        LocalDate startDate = context.startTime.toLocalDate();
        LocalDate endDate = context.endTime.toLocalDate();
        try {
            ChatDashboardSummary chatSummary = chatDashboardQueryService.querySummary(startDate, endDate, context.tenantFilterId);
            summary.setChatMessageTotal(chatSummary.messageTotal());
            summary.setChatSessionTotal(chatSummary.sessionTotal());
            summary.setActiveAssistantTotal(chatSummary.activeAssistantTotal());
            summary.setChatActiveUserTotal(chatSummary.chatActiveUserTotal());

            List<ChatDashboardTrend> rawTrends = useHourlyBuckets(context)
                    ? chatDashboardQueryService.queryHourlyTrends(context.startTime, context.endTime, context.tenantFilterId)
                    : chatDashboardQueryService.queryTrends(startDate, endDate, context.tenantFilterId);
            List<DashboardTrendItemVo> trends = buildTrendItems(context, rawTrends);

            List<ChatAssistantUsageRank> rawRanks = chatDashboardQueryService.queryAssistantUsageRanks(
                    startDate,
                    endDate,
                    context.tenantFilterId,
                    DEFAULT_ASSISTANT_TREND_LIMIT
            );
            List<DashboardAssistantTrendSeriesVo> assistantTrends = buildAssistantTrendSeries(
                    context,
                    rawRanks
            );
            List<DashboardDistributionItemVo> distribution = buildAssistantDistribution(
                    rawRanks.subList(0, Math.min(DEFAULT_ASSISTANT_RANK_LIMIT, rawRanks.size()))
            );
            return new ChatDashboardPayload(chatStatus, trends, assistantTrends, distribution);
        } catch (Exception ex) {
            log.warn("加载工作台聊天统计失败，已降级为不可用状态，range={}, tenantId={}",
                    context.range,
                    context.tenantFilterId,
                    ex);
            chatStatus.setAvailable(Boolean.FALSE);
            chatStatus.setMessage(CHAT_UNAVAILABLE_MESSAGE);
            summary.setChatMessageTotal(0L);
            summary.setChatSessionTotal(0L);
            summary.setActiveAssistantTotal(0L);
            summary.setChatActiveUserTotal(0L);
            return new ChatDashboardPayload(
                    chatStatus,
                    new ArrayList<>(),
                    new ArrayList<>(),
                    new ArrayList<>()
            );
        }
    }

    /**
     * 构建聊天趋势项，缺失日期补 0。
     *
     * @param range 时间范围
     * @param rawTrends 原始趋势
     * @return 趋势项
     */
    private List<DashboardTrendItemVo> buildTrendItems(DashboardQueryContext context, List<ChatDashboardTrend> rawTrends) {
        List<TimeBucket> buckets = buildBuckets(
                context.range,
                context.startTime.toLocalDate(),
                context.endTime.toLocalDate().minusDays(1)
        );
        Map<String, ChatDashboardTrend> trendMap = new HashMap<>();
        for (ChatDashboardTrend rawTrend : rawTrends) {
            trendMap.put(rawTrend.bucketKey(), rawTrend);
        }

        List<DashboardTrendItemVo> items = new ArrayList<>(buckets.size());
        for (TimeBucket bucket : buckets) {
            ChatDashboardTrend trend = trendMap.get(bucket.key);
            DashboardTrendItemVo item = new DashboardTrendItemVo();
            item.setKey(bucket.key);
            item.setLabel(bucket.label);
            item.setActiveUserTotal(trend == null ? 0L : trend.activeUserTotal());
            item.setChatMessageTotal(trend == null ? 0L : trend.messageTotal());
            item.setChatSessionTotal(trend == null ? 0L : trend.sessionTotal());
            items.add(item);
        }
        return items;
    }

    /**
     * 构建智能体使用排行。
     *
     * @param ranks 原始排行数据
     * @return 页面排行项
     */
    private List<DashboardDistributionItemVo> buildAssistantDistribution(List<ChatAssistantUsageRank> ranks) {
        List<DashboardDistributionItemVo> items = new ArrayList<>(ranks.size());
        for (ChatAssistantUsageRank rank : ranks) {
            DashboardDistributionItemVo item = new DashboardDistributionItemVo();
            item.setKey(rank.assistantId() == null ? "" : rank.assistantId().toString());
            item.setAssistantId(rank.assistantId());
            item.setLabel(resolveAssistantLabel(rank.assistantId(), rank.assistantName()));
            item.setUserTotal(rank.userTotal());
            item.setMessageTotal(rank.messageTotal());
            item.setSessionTotal(rank.sessionTotal());
            item.setAvgMessagePerSession(calculateAvg(rank.messageTotal(), rank.sessionTotal()));
            item.setAvgSessionPerUser(calculateAvg(rank.sessionTotal(), rank.userTotal()));
            item.setValue(rank.sessionTotal());
            items.add(item);
        }
        return items;
    }

    /**
     * 构建智能体活跃趋势序列。
     *
     * @param context 查询上下文
     * @param ranks 智能体排行
     * @return 趋势序列
     */
    private List<DashboardAssistantTrendSeriesVo> buildAssistantTrendSeries(DashboardQueryContext context,
                                                                            List<ChatAssistantUsageRank> ranks) {
        if (ranks == null || ranks.isEmpty()) {
            return new ArrayList<>();
        }
        List<TimeBucket> buckets = buildBuckets(
                context.range,
                context.startTime.toLocalDate(),
                context.endTime.toLocalDate().minusDays(1)
        );
        Map<BigInteger, ChatAssistantUsageRank> rankMap = new LinkedHashMap<>();
        for (ChatAssistantUsageRank rank : ranks) {
            rankMap.putIfAbsent(rank.assistantId(), rank);
        }
        List<BigInteger> assistantIds = new ArrayList<>(rankMap.keySet());
        List<ChatAssistantSessionTrend> rawAssistantTrends = useHourlyBuckets(context)
                ? chatDashboardQueryService.queryAssistantHourlyTrends(
                context.startTime,
                context.endTime,
                context.tenantFilterId,
                assistantIds
        )
                : chatDashboardQueryService.queryAssistantTrends(
                context.startTime.toLocalDate(),
                context.endTime.toLocalDate(),
                context.tenantFilterId,
                assistantIds
        );

        Map<BigInteger, Map<String, ChatAssistantSessionTrend>> trendMap = new HashMap<>();
        for (ChatAssistantSessionTrend rawTrend : rawAssistantTrends) {
            trendMap.computeIfAbsent(rawTrend.assistantId(), key -> new HashMap<>())
                    .put(rawTrend.bucketKey(), rawTrend);
        }

        List<DashboardAssistantTrendSeriesVo> seriesList = new ArrayList<>(rankMap.size());
        for (ChatAssistantUsageRank rank : rankMap.values()) {
            BigInteger assistantId = rank.assistantId();
            DashboardAssistantTrendSeriesVo series = new DashboardAssistantTrendSeriesVo();
            series.setAssistantId(assistantId);
            series.setLabel(resolveAssistantLabel(assistantId, rank.assistantName()));
            series.setTotalSessionCount(rank.sessionTotal());

            List<DashboardAssistantTrendPointVo> points = new ArrayList<>(buckets.size());
            Map<String, ChatAssistantSessionTrend> assistantTrendMap =
                    trendMap.getOrDefault(assistantId, new HashMap<>());
            for (TimeBucket bucket : buckets) {
                ChatAssistantSessionTrend trend = assistantTrendMap.get(bucket.key);
                DashboardAssistantTrendPointVo point = new DashboardAssistantTrendPointVo();
                point.setKey(bucket.key);
                point.setLabel(bucket.label);
                point.setSessionTotal(trend == null ? 0L : trend.sessionTotal());
                points.add(point);
            }
            series.setPoints(points);
            seriesList.add(series);
        }
        return seriesList;
    }

    /**
     * 查询用户活跃排行。
     *
     * @param context 查询上下文
     * @param limit 返回条数，空表示全部
     * @return 用户活跃排行
     */
    private List<DashboardUserRankItemVo> queryUserRanks(DashboardQueryContext context, Integer limit) {
        if (!chatDashboardQueryService.available()) {
            return new ArrayList<>();
        }
        List<ChatActiveUserRank> rawUserRanks = chatDashboardQueryService.queryActiveUserRanks(
                context.startTime.toLocalDate(),
                context.endTime.toLocalDate(),
                context.tenantFilterId,
                context.assistantId,
                limit
        );
        return buildUserRanks(rawUserRanks);
    }

    /**
     * 构建用户活跃排行。
     *
     * @param ranks 原始排行数据
     * @return 页面排行项
     */
    private List<DashboardUserRankItemVo> buildUserRanks(List<ChatActiveUserRank> ranks) {
        if (ranks == null || ranks.isEmpty()) {
            return new ArrayList<>();
        }
        List<DashboardUserRankItemVo> items = new ArrayList<>(ranks.size());
        Map<BigInteger, AccountIdentitySnapshot> identityMap = resolveAccountIdentityMap(ranks);
        for (ChatActiveUserRank rank : ranks) {
            ResolvedUserIdentity identity = resolveUserIdentity(
                    rank.userId(),
                    rank.userAccount(),
                    identityMap.get(rank.userId())
            );
            DashboardUserRankItemVo item = new DashboardUserRankItemVo();
            item.setUserId(rank.userId());
            item.setLabel(identity.label);
            item.setLoginName(identity.loginName);
            item.setNickname(identity.nickname);
            item.setSessionTotal(rank.sessionTotal());
            item.setMessageTotal(rank.messageTotal());
            items.add(item);
        }
        return items;
    }

    /**
     * 构建导出表头。
     *
     * @return 表头
     */
    private List<List<String>> buildUserRankExportHead() {
        List<List<String>> head = new ArrayList<>(4);
        head.add(List.of("登录账号"));
        head.add(List.of("昵称"));
        head.add(List.of("会话数"));
        head.add(List.of("消息数"));
        return head;
    }

    /**
     * 构建导出数据行。
     *
     * @param userRanks 用户排行
     * @return 数据行
     */
    private List<List<String>> buildUserRankExportRows(List<DashboardUserRankItemVo> userRanks) {
        List<List<String>> rows = new ArrayList<>(userRanks.size());
        for (DashboardUserRankItemVo item : userRanks) {
            List<String> row = new ArrayList<>(4);
            row.add(defaultIfBlank(item.getLoginName()));
            row.add(defaultIfBlank(item.getNickname()));
            row.add(String.valueOf(item.getSessionTotal() == null ? 0L : item.getSessionTotal()));
            row.add(String.valueOf(item.getMessageTotal() == null ? 0L : item.getMessageTotal()));
            rows.add(row);
        }
        return rows;
    }

    /**
     * 按租户统计平台资源数量。
     *
     * @param tableName 表名
     * @param alias 别名
     * @param containsLogicDelete 是否包含逻辑删除条件
     * @param context 查询上下文
     * @return 统计值
     */
    private long countScopedTable(String tableName, String alias, boolean containsLogicDelete, DashboardQueryContext context) {
        StringBuilder sql = new StringBuilder();
        List<Object> params = new ArrayList<>();

        sql.append("SELECT COUNT(*) AS total FROM ").append(tableName).append(" ").append(alias).append(" WHERE 1 = 1 ");
        if (containsLogicDelete) {
            sql.append("AND ").append(alias).append(".is_deleted IS NULL ");
        }
        appendOptionalTenantFilter(sql, params, context.tenantFilterId, alias + ".tenant_id");
        appendOptionalDeptFilter(sql, params, context.deptFilterId, alias + ".dept_id");
        return queryForLong(sql.toString(), params);
    }

    /**
     * 统计当前时间范围内活跃用户数。
     *
     * @param context 查询上下文
     * @return 活跃用户数
     */
    private long countActiveUsers(DashboardQueryContext context) {
        StringBuilder sql = new StringBuilder();
        List<Object> params = new ArrayList<>();

        sql.append("SELECT COUNT(DISTINCT l.account_id) AS total ")
                .append("FROM tb_sys_log l ")
                .append("INNER JOIN tb_sys_account a ON a.id = l.account_id AND a.is_deleted IS NULL ")
                .append("WHERE l.created >= ? AND l.created < ? ");
        params.add(toDate(context.startTime));
        params.add(toDate(context.endTime));
        appendOptionalTenantFilter(sql, params, context.tenantFilterId, "a.tenant_id");
        appendOptionalDeptFilter(sql, params, context.deptFilterId, "a.dept_id");
        return queryForLong(sql.toString(), params);
    }

    /**
     * 执行 count SQL 并返回 long 值。
     *
     * @param sql SQL
     * @param params 参数
     * @return long 值
     */
    private long queryForLong(String sql, List<Object> params) {
        Object result = Db.selectObject(sql, params.toArray());
        return asLong(result);
    }

    /**
     * 追加租户过滤。
     *
     * @param sql SQL 构造器
     * @param params 参数列表
     * @param tenantId 租户 ID
     * @param columnName 列名
     */
    private void appendOptionalTenantFilter(StringBuilder sql, List<Object> params, BigInteger tenantId, String columnName) {
        if (tenantId != null) {
            sql.append(" AND ").append(columnName).append(" = ? ");
            params.add(tenantId);
        }
    }

    /**
     * 追加部门过滤。
     *
     * @param sql SQL 构造器
     * @param params 参数列表
     * @param deptId 部门 ID
     * @param columnName 列名
     */
    private void appendOptionalDeptFilter(StringBuilder sql, List<Object> params, BigInteger deptId, String columnName) {
        if (deptId != null) {
            sql.append(" AND ").append(columnName).append(" = ? ");
            params.add(deptId);
        }
    }

    /**
     * 构建查询上下文。
     *
     * @param loginAccount 当前登录账号
     * @param query 查询条件
     * @return 查询上下文
     */
    private DashboardQueryContext buildContext(LoginAccount loginAccount, DashboardOverviewQuery query) {
        return buildContext(
                loginAccount,
                query == null ? null : query.getRange(),
                query == null ? null : query.getStartDate(),
                query == null ? null : query.getEndDate()
        );
    }

    /**
     * 构建用户榜查询上下文。
     *
     * @param loginAccount 当前登录账号
     * @param query 用户榜查询
     * @return 查询上下文
     */
    private DashboardQueryContext buildContext(LoginAccount loginAccount, DashboardUserRankQuery query) {
        DashboardQueryContext context = buildContext(
                loginAccount,
                query == null ? null : query.getRange(),
                query == null ? null : query.getStartDate(),
                query == null ? null : query.getEndDate()
        );
        context.assistantId = validateAssistantId(loginAccount, query == null ? null : query.getAssistantId());
        return context;
    }

    /**
     * 构建基础查询上下文。
     *
     * @param loginAccount 当前登录账号
     * @param range 时间范围
     * @param startDate 开始日期
     * @param endDate 结束日期
     * @return 查询上下文
     */
    private DashboardQueryContext buildContext(LoginAccount loginAccount,
                                               String range,
                                               String startDate,
                                               String endDate) {
        DashboardQueryContext context = new DashboardQueryContext();
        context.range = normalizeRange(range);
        context.superAdmin = isSuperAdmin(loginAccount);

        LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
        if ("today".equals(context.range)) {
            context.startTime = LocalDateTime.of(today, LocalTime.MIN);
            context.endTime = context.startTime.plusDays(1);
            context.queryStartDate = today.toString();
            context.queryEndDate = today.toString();
        } else if ("7d".equals(context.range)) {
            context.startTime = LocalDateTime.of(today.minusDays(6), LocalTime.MIN);
            context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
            context.queryStartDate = today.minusDays(6).toString();
            context.queryEndDate = today.toString();
        } else if ("30d".equals(context.range)) {
            context.startTime = LocalDateTime.of(today.minusDays(29), LocalTime.MIN);
            context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
            context.queryStartDate = today.minusDays(29).toString();
            context.queryEndDate = today.toString();
        } else {
            LocalDate customStartDate = parseRequiredDate(startDate, "开始日期不能为空");
            LocalDate customEndDate = parseRequiredDate(endDate, "结束日期不能为空");
            if (customStartDate.isAfter(customEndDate)) {
                throw new BusinessException("开始日期不能晚于结束日期");
            }
            context.startTime = LocalDateTime.of(customStartDate, LocalTime.MIN);
            context.endTime = LocalDateTime.of(customEndDate.plusDays(1), LocalTime.MIN);
            context.queryStartDate = customStartDate.toString();
            context.queryEndDate = customEndDate.toString();
        }

        context.tenantFilterId = context.superAdmin || loginAccount == null ? null : loginAccount.getTenantId();
        return context;
    }

    /**
     * 判断当前登录账号是否为超管。
     *
     * @param loginAccount 当前登录账号
     * @return true 表示超管
     */
    private boolean isSuperAdmin(LoginAccount loginAccount) {
        if (loginAccount == null || loginAccount.getId() == null) {
            return false;
        }
        QueryWrapper roleMappingWrapper = QueryWrapper.create();
        roleMappingWrapper.eq(SysAccountRole::getAccountId, loginAccount.getId());
        List<BigInteger> roleIds = sysAccountRoleService.list(roleMappingWrapper)
                .stream()
                .map(SysAccountRole::getRoleId)
                .collect(Collectors.toList());
        if (roleIds.isEmpty()) {
            return Constants.SUPER_ADMIN_ID.equals(loginAccount.getId());
        }

        QueryWrapper roleWrapper = QueryWrapper.create();
        roleWrapper.in(SysRole::getId, roleIds);
        roleWrapper.eq(SysRole::getRoleKey, Constants.SUPER_ADMIN_ROLE_CODE);
        return sysRoleService.count(roleWrapper) > 0;
    }

    /**
     * 归一化时间范围参数。
     *
     * @param range 原始时间范围
     * @return 规范化后的时间范围
     */
    private String normalizeRange(String range) {
        if (!StringUtils.hasText(range)) {
            return "7d";
        }
        if ("today".equals(range) || "7d".equals(range) || "30d".equals(range) || "custom".equals(range)) {
            return range;
        }
        throw new BusinessException("不支持的时间范围: " + range);
    }

    /**
     * 构建时间桶。
     *
     * @param range 时间范围
     * @return 时间桶列表
     */
    private List<TimeBucket> buildBuckets(String range, LocalDate customStartDate, LocalDate customEndDate) {
        List<TimeBucket> buckets = new ArrayList<>();
        LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
        boolean hourlyBucket = "today".equals(range)
                || ("custom".equals(range)
                && customStartDate != null
                && customEndDate != null
                && customStartDate.equals(customEndDate));
        if (hourlyBucket) {
            DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00");
            DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("HH:00");
            LocalDate bucketDate = "today".equals(range) ? today : customStartDate;
            LocalDateTime start = LocalDateTime.of(bucketDate, LocalTime.MIN);
            for (int hour = 0; hour < 24; hour++) {
                LocalDateTime current = start.plusHours(hour);
                buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
            }
            return buckets;
        }

        DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("MM-dd");
        int days;
        LocalDate start;
        if ("7d".equals(range)) {
            days = 7;
            start = today.minusDays(6);
        } else if ("30d".equals(range)) {
            days = 30;
            start = today.minusDays(29);
        } else {
            start = customStartDate;
            days = (int) java.time.temporal.ChronoUnit.DAYS.between(customStartDate, customEndDate) + 1;
        }
        for (int i = 0; i < days; i++) {
            LocalDate current = start.plusDays(i);
            buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
        }
        return buckets;
    }

    /**
     * 当前上下文是否按小时构建趋势。
     *
     * @param context 查询上下文
     * @return true 表示按小时
     */
    private boolean useHourlyBuckets(DashboardQueryContext context) {
        return "today".equals(context.range)
                || ("custom".equals(context.range)
                && context.startTime != null
                && context.endTime != null
                && context.startTime.toLocalDate().equals(context.endTime.toLocalDate().minusDays(1)));
    }

    /**
     * 解析必填日期参数。
     *
     * @param dateText 日期文本
     * @param errorMessage 错误信息
     * @return 日期
     */
    private LocalDate parseRequiredDate(String dateText, String errorMessage) {
        if (!StringUtils.hasText(dateText)) {
            throw new BusinessException(errorMessage);
        }
        try {
            return LocalDate.parse(dateText.trim());
        } catch (Exception ex) {
            throw new BusinessException("日期格式不正确: " + dateText);
        }
    }

    /**
     * 把 LocalDateTime 转换为 Date。
     *
     * @param dateTime 时间
     * @return Date
     */
    private Date toDate(LocalDateTime dateTime) {
        return Date.from(dateTime.atZone(DEFAULT_ZONE_ID).toInstant());
    }

    /**
     * 解析对象为 long 值。
     *
     * @param value 原始对象
     * @return long 值
     */
    private long asLong(Object value) {
        if (value == null) {
            return 0L;
        }
        if (value instanceof Number) {
            return ((Number) value).longValue();
        }
        return Long.parseLong(String.valueOf(value));
    }

    /**
     * 计算平均每会话消息数。
     *
     * @param messageTotal 消息总数
     * @param sessionTotal 会话总数
     * @return 平均值
     */
    private double calculateAvg(long messageTotal, long sessionTotal) {
        if (sessionTotal <= 0) {
            return 0D;
        }
        return (double) messageTotal / (double) sessionTotal;
    }

    /**
     * 批量解析用户展示名称映射。
     *
     * @param ranks 活跃排行
     * @return 名称映射
     */
    private Map<BigInteger, AccountIdentitySnapshot> resolveAccountIdentityMap(List<ChatActiveUserRank> ranks) {
        List<BigInteger> userIds = ranks.stream()
                .map(ChatActiveUserRank::userId)
                .filter(java.util.Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (userIds.isEmpty()) {
            return new HashMap<>();
        }
        QueryWrapper queryWrapper = QueryWrapper.create()
                .select(SysAccount::getId, SysAccount::getLoginName, SysAccount::getNickname)
                .in(SysAccount::getId, userIds);
        List<SysAccount> accounts = sysAccountService.list(queryWrapper);
        Map<BigInteger, AccountIdentitySnapshot> identityMap = new HashMap<>(userIds.size());
        if (accounts == null) {
            return identityMap;
        }
        for (SysAccount account : accounts) {
            if (account == null || account.getId() == null) {
                continue;
            }
            identityMap.put(
                    account.getId(),
                    new AccountIdentitySnapshot(trimToNull(account.getLoginName()), trimToNull(account.getNickname()))
            );
        }
        return identityMap;
    }

    /**
     * 解析智能体展示名称。
     *
     * @param assistantId 智能体 ID
     * @param assistantName 智能体名称
     * @return 展示名称
     */
    private String resolveAssistantLabel(BigInteger assistantId, String assistantName) {
        if (StringUtils.hasText(assistantName)) {
            return assistantName.trim();
        }
        return assistantId == null ? "智能体-未知" : "智能体-" + assistantId;
    }

    /**
     * 解析用户展示名称。
     *
     * @param userId 用户 ID
     * @param userAccount 聊天侧账号快照
     * @param snapshot 系统账号快照
     * @return 用户身份
     */
    private ResolvedUserIdentity resolveUserIdentity(BigInteger userId,
                                                     String userAccount,
                                                     AccountIdentitySnapshot snapshot) {
        String loginName = snapshot == null ? null : snapshot.loginName;
        String nickname = snapshot == null ? null : snapshot.nickname;
        String trimmedUserAccount = trimToNull(userAccount);
        if (StringUtils.hasText(loginName)) {
            if (StringUtils.hasText(nickname)) {
                return new ResolvedUserIdentity(loginName, nickname, loginName + "（" + nickname + "）");
            }
            return new ResolvedUserIdentity(loginName, nickname, loginName);
        }
        if (StringUtils.hasText(trimmedUserAccount)) {
            return new ResolvedUserIdentity(trimmedUserAccount, nickname, trimmedUserAccount);
        }
        return new ResolvedUserIdentity(null, nickname, userId == null ? "用户-未知" : "用户-" + userId);
    }

    /**
     * 校验智能体筛选条件。
     *
     * @param loginAccount 当前登录账号
     * @param assistantId 智能体 ID
     * @return 规范化后的智能体 ID
     */
    private BigInteger validateAssistantId(LoginAccount loginAccount, BigInteger assistantId) {
        if (assistantId == null) {
            return null;
        }
        Bot bot = botService.getById(assistantId);
        if (bot == null || !Integer.valueOf(1).equals(bot.getStatus())) {
            throw new BusinessException("聊天助手不存在或未启用");
        }
        boolean visible = categoryPermissionService.canAccessCategory(
                loginAccount,
                "BOT",
                bot.getCreatedBy(),
                bot.getCategoryId()
        );
        if (!visible) {
            throw new BusinessException("聊天助手不存在或未启用");
        }
        return assistantId;
    }

    /**
     * 归一化文本。
     *
     * @param value 原始文本
     * @return 去空白后的文本
     */
    private String trimToNull(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        return value.trim();
    }

    /**
     * 把空文本转为空串。
     *
     * @param value 原始文本
     * @return 输出文本
     */
    private String defaultIfBlank(String value) {
        return StringUtils.hasText(value) ? value.trim() : "";
    }

    /**
     * 工作台查询上下文。
     */
    private static class DashboardQueryContext {
        private String range;
        private BigInteger tenantFilterId;
        private BigInteger deptFilterId;
        private BigInteger assistantId;
        private boolean superAdmin;
        private LocalDateTime startTime;
        private LocalDateTime endTime;
        private String queryStartDate;
        private String queryEndDate;
    }

    /**
     * 时间桶。
     */
    private static class TimeBucket {
        private final String key;
        private final String label;

        private TimeBucket(String key, String label) {
            this.key = key;
            this.label = label;
        }
    }

    /**
     * 系统账号身份快照。
     */
    private static class AccountIdentitySnapshot {
        private final String loginName;
        private final String nickname;

        private AccountIdentitySnapshot(String loginName, String nickname) {
            this.loginName = loginName;
            this.nickname = nickname;
        }
    }

    /**
     * 用户展示身份。
     */
    private static class ResolvedUserIdentity {
        private final String loginName;
        private final String nickname;
        private final String label;

        private ResolvedUserIdentity(String loginName, String nickname, String label) {
            this.loginName = loginName;
            this.nickname = nickname;
            this.label = label;
        }
    }

    /**
     * 聊天统计页面载荷。
     */
    private record ChatDashboardPayload(
            DashboardChatStatusVo chatStatus,
            List<DashboardTrendItemVo> trends,
            List<DashboardAssistantTrendSeriesVo> assistantTrends,
            List<DashboardDistributionItemVo> distribution
    ) {
    }
}
