package tech.easyflow.chatlog.repository.analyticaldb;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
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.domain.dto.ChatHistoryPage;
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery;
import tech.easyflow.chatlog.support.ChatJsonSupport;
import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations;
import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageRequest;
import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageResult;
import tech.easyflow.common.analyticaldb.support.AnalyticalDBHealthSupport;

import java.math.BigInteger;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;

@Repository
public class ChatAnalyticalDBRepository {

    private static final DateTimeFormatter CH_DATE_TIME_FORMATTER =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private final AnalyticalDBOperations analyticalDBOperations;
    private final AnalyticalDBHealthSupport analyticalDBHealthSupport;
    private final ChatJsonSupport jsonSupport;

    public ChatAnalyticalDBRepository(ObjectProvider<AnalyticalDBOperations> analyticalDBOperationsProvider,
                                      AnalyticalDBHealthSupport analyticalDBHealthSupport,
                                      ChatJsonSupport jsonSupport) {
        this.analyticalDBOperations = analyticalDBOperationsProvider.getIfAvailable();
        this.analyticalDBHealthSupport = analyticalDBHealthSupport;
        this.jsonSupport = jsonSupport;
    }

    public boolean enabled() {
        return analyticalDBOperations != null && analyticalDBOperations.available();
    }

    public void assertAvailable() {
        if (!enabled()) {
            throw new IllegalStateException("AnalyticalDB 数据源未启用");
        }
    }

    public void selfCheck() {
        analyticalDBHealthSupport.selfCheck();
    }

    public void upsertSessions(List<ChatSessionSummary> sessions) {
        if (!enabled() || sessions.isEmpty()) {
            return;
        }
        analyticalDBOperations.batchUpdate("INSERT INTO ods_chat_session (" +
                        "id, tenant_id, dept_id, user_id, user_account, assistant_id, assistant_code, assistant_name, title, " +
                        "last_message_preview, last_sender_id, last_sender_name, message_count, access_at, last_message_at, " +
                        "created, created_by, modified, modified_by, is_deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                sessions,
                sessions.size(),
                sessionStatementSetter());
    }

    public void appendLogs(List<ChatMessageRecord> records) {
        if (!enabled() || records.isEmpty()) {
            return;
        }
        analyticalDBOperations.batchUpdate("INSERT INTO ods_chat_log (" +
                        "id, session_id, user_id, assistant_id, sender_id, sender_name, sender_role, content_type, content_text, " +
                        "content_payload, created, created_by, sync_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                records,
                records.size(),
                logStatementSetter());
    }

    public ChatHistoryPage queryHistory(BigInteger sessionId, ChatPageQuery query) {
        assertAvailable();
        AnalyticalDBPageResult<ChatMessageRecord> pageResult = analyticalDBOperations.page(
                "SELECT COUNT(1) FROM ods_chat_log FINAL WHERE session_id=?",
                new Object[]{sessionId},
                "SELECT id, session_id, user_id, assistant_id, sender_id, sender_name, sender_role, content_type, content_text, " +
                        "content_payload, created, created_by, sync_version " +
                        "FROM ods_chat_log FINAL WHERE session_id=? ORDER BY created DESC, id DESC",
                new Object[]{sessionId},
                new AnalyticalDBPageRequest(Math.toIntExact(query.getPageNumber()), Math.toIntExact(query.getPageSize())),
                (rs, rowNum) -> {
                    ChatMessageRecord record = new ChatMessageRecord();
                    record.setId(bigInteger(rs.getObject("id")));
                    record.setSessionId(bigInteger(rs.getObject("session_id")));
                    record.setUserId(bigInteger(rs.getObject("user_id")));
                    record.setAssistantId(bigInteger(rs.getObject("assistant_id")));
                    record.setSenderId(bigInteger(rs.getObject("sender_id")));
                    record.setSenderName(rs.getString("sender_name"));
                    record.setSenderRole(rs.getString("sender_role"));
                    record.setContentType(rs.getString("content_type"));
                    record.setContentText(rs.getString("content_text"));
                    record.setContentPayload(jsonSupport.toMap(rs.getString("content_payload")));
                    record.setCreated(rs.getTimestamp("created"));
                    record.setCreatedBy(bigInteger(rs.getObject("created_by")));
                    record.setSyncVersion(rs.getLong("sync_version"));
                    return record;
                }
        );
        ChatHistoryPage page = new ChatHistoryPage();
        page.setTotal(pageResult.getTotal());
        page.setPageNumber(pageResult.getPageNumber());
        page.setPageSize(pageResult.getPageSize());
        page.setRecords(pageResult.getRecords());
        return page;
    }

    public ChatSessionPage pageSessions(ChatSessionFilterQuery query) {
        assertAvailable();
        List<Object> countArgs = new java.util.ArrayList<>();
        StringBuilder countSql = new StringBuilder("SELECT COUNT(1) FROM ods_chat_session FINAL WHERE is_deleted=0");
        List<Object> dataArgs = new java.util.ArrayList<>();
        StringBuilder dataSql = new StringBuilder("SELECT id, tenant_id, dept_id, user_id, user_account, assistant_id, assistant_code, assistant_name, title, " +
                "last_message_preview, last_sender_id, last_sender_name, message_count, access_at, last_message_at, created, created_by, modified, modified_by, is_deleted " +
                "FROM ods_chat_session FINAL WHERE is_deleted=0");
        appendSessionFilters(query, countSql, countArgs);
        appendSessionFilters(query, dataSql, dataArgs);
        dataSql.append(" ORDER BY access_at DESC, id DESC");

        AnalyticalDBPageResult<ChatSessionSummary> pageResult = analyticalDBOperations.page(
                countSql.toString(),
                countArgs.toArray(),
                dataSql.toString(),
                dataArgs.toArray(),
                new AnalyticalDBPageRequest(Math.toIntExact(query.getPageNumber()), Math.toIntExact(query.getPageSize())),
                (rs, rowNum) -> {
                    ChatSessionSummary summary = new ChatSessionSummary();
                    summary.setId(bigInteger(rs.getObject("id")));
                    summary.setTenantId(bigInteger(rs.getObject("tenant_id")));
                    summary.setDeptId(bigInteger(rs.getObject("dept_id")));
                    summary.setUserId(bigInteger(rs.getObject("user_id")));
                    summary.setUserAccount(rs.getString("user_account"));
                    summary.setAssistantId(bigInteger(rs.getObject("assistant_id")));
                    summary.setAssistantCode(rs.getString("assistant_code"));
                    summary.setAssistantName(rs.getString("assistant_name"));
                    summary.setTitle(rs.getString("title"));
                    summary.setLastMessagePreview(rs.getString("last_message_preview"));
                    summary.setLastSenderId(bigInteger(rs.getObject("last_sender_id")));
                    summary.setLastSenderName(rs.getString("last_sender_name"));
                    summary.setMessageCount(rs.getInt("message_count"));
                    summary.setAccessAt(rs.getTimestamp("access_at"));
                    summary.setLastMessageAt(rs.getTimestamp("last_message_at"));
                    summary.setCreated(rs.getTimestamp("created"));
                    summary.setCreatedBy(bigInteger(rs.getObject("created_by")));
                    summary.setModified(rs.getTimestamp("modified"));
                    summary.setModifiedBy(bigInteger(rs.getObject("modified_by")));
                    summary.setIsDeleted(rs.getInt("is_deleted"));
                    return summary;
                }
        );
        ChatSessionPage page = new ChatSessionPage();
        page.setTotal(pageResult.getTotal());
        page.setPageNumber(pageResult.getPageNumber());
        page.setPageSize(pageResult.getPageSize());
        page.setRecords(pageResult.getRecords());
        return page;
    }

    public ChatSessionSummary getSession(BigInteger sessionId) {
        assertAvailable();
        return analyticalDBOperations.queryOne(
                "SELECT id, tenant_id, dept_id, user_id, user_account, assistant_id, assistant_code, assistant_name, title, " +
                        "last_message_preview, last_sender_id, last_sender_name, message_count, access_at, last_message_at, created, created_by, modified, modified_by, is_deleted " +
                        "FROM ods_chat_session FINAL WHERE id=? LIMIT 1",
                (rs, rowNum) -> {
                    ChatSessionSummary summary = new ChatSessionSummary();
                    summary.setId(bigInteger(rs.getObject("id")));
                    summary.setTenantId(bigInteger(rs.getObject("tenant_id")));
                    summary.setDeptId(bigInteger(rs.getObject("dept_id")));
                    summary.setUserId(bigInteger(rs.getObject("user_id")));
                    summary.setUserAccount(rs.getString("user_account"));
                    summary.setAssistantId(bigInteger(rs.getObject("assistant_id")));
                    summary.setAssistantCode(rs.getString("assistant_code"));
                    summary.setAssistantName(rs.getString("assistant_name"));
                    summary.setTitle(rs.getString("title"));
                    summary.setLastMessagePreview(rs.getString("last_message_preview"));
                    summary.setLastSenderId(bigInteger(rs.getObject("last_sender_id")));
                    summary.setLastSenderName(rs.getString("last_sender_name"));
                    summary.setMessageCount(rs.getInt("message_count"));
                    summary.setAccessAt(rs.getTimestamp("access_at"));
                    summary.setLastMessageAt(rs.getTimestamp("last_message_at"));
                    summary.setCreated(rs.getTimestamp("created"));
                    summary.setCreatedBy(bigInteger(rs.getObject("created_by")));
                    summary.setModified(rs.getTimestamp("modified"));
                    summary.setModifiedBy(bigInteger(rs.getObject("modified_by")));
                    summary.setIsDeleted(rs.getInt("is_deleted"));
                    return summary;
                },
                sessionId
        );
    }

    /**
     * 查询聊天工作台汇总统计。
     *
     * @param startDate 开始日期，包含
     * @param endDate 结束日期，不包含
     * @param tenantId 租户 ID，空表示全局
     * @return 聊天汇总统计
     */
    public ChatDashboardSummary queryDashboardSummary(LocalDate startDate, LocalDate endDate, BigInteger tenantId) {
        assertAvailable();
        List<Object> args = new java.util.ArrayList<>();
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT ")
                .append("uniqExact(agg.dimension_id) AS session_total, ")
                .append("ifNull(sum(agg.message_count), 0) AS message_total, ")
                .append("uniqExact(agg.assistant_id) AS active_assistant_total, ")
                .append("uniqExact(agg.user_id) AS active_user_total ")
                .append("FROM dws_chat_session_day agg ")
                .append("LEFT JOIN (SELECT id, tenant_id FROM ods_chat_session FINAL) s ON s.id = agg.dimension_id ")
                .append("WHERE agg.stat_date >= toDate(?) AND agg.stat_date < toDate(?)");
        args.add(startDate.toString());
        args.add(endDate.toString());
        appendOptionalTenantFilter(sql, args, tenantId, "s.tenant_id");

        ChatDashboardSummary summary = analyticalDBOperations.queryOne(
                sql.toString(),
                (rs, rowNum) -> new ChatDashboardSummary(
                        rs.getLong("session_total"),
                        rs.getLong("message_total"),
                        rs.getLong("active_assistant_total"),
                        rs.getLong("active_user_total")
                ),
                args.toArray()
        );
        return summary == null ? ChatDashboardSummary.empty() : summary;
    }

    /**
     * 查询聊天工作台日趋势。
     *
     * @param startDate 开始日期，包含
     * @param endDate 结束日期，不包含
     * @param tenantId 租户 ID，空表示全局
     * @return 日趋势列表
     */
    public List<ChatDashboardTrend> queryDashboardTrends(LocalDate startDate, LocalDate endDate, BigInteger tenantId) {
        assertAvailable();
        List<Object> args = new java.util.ArrayList<>();
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT toString(agg.stat_date) AS bucket_key, ")
                .append("uniqExact(agg.dimension_id) AS session_total, ")
                .append("ifNull(sum(agg.message_count), 0) AS message_total, ")
                .append("uniqExact(agg.user_id) AS active_user_total ")
                .append("FROM dws_chat_session_day agg ")
                .append("LEFT JOIN (SELECT id, tenant_id FROM ods_chat_session FINAL) s ON s.id = agg.dimension_id ")
                .append("WHERE agg.stat_date >= toDate(?) AND agg.stat_date < toDate(?)");
        args.add(startDate.toString());
        args.add(endDate.toString());
        appendOptionalTenantFilter(sql, args, tenantId, "s.tenant_id");
        sql.append(" GROUP BY agg.stat_date ORDER BY agg.stat_date ASC");

        return analyticalDBOperations.query(
                sql.toString(),
                (rs, rowNum) -> new ChatDashboardTrend(
                        rs.getString("bucket_key"),
                        rs.getLong("session_total"),
                        rs.getLong("message_total"),
                        rs.getLong("active_user_total")
                ),
                args.toArray()
        );
    }

    /**
     * 查询聊天工作台小时趋势。
     *
     * @param startTime 开始时间，包含
     * @param endTime 结束时间，不包含
     * @param tenantId 租户 ID，空表示全局
     * @return 小时趋势列表
     */
    public List<ChatDashboardTrend> queryDashboardHourlyTrends(LocalDateTime startTime,
                                                               LocalDateTime endTime,
                                                               BigInteger tenantId) {
        assertAvailable();
        List<Object> args = new java.util.ArrayList<>();
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT formatDateTime(toStartOfHour(l.created), '%Y-%m-%d %H:00:00') AS bucket_key, ")
                .append("count() AS message_total, ")
                .append("uniqExact(l.session_id) AS session_total, ")
                .append("uniqExact(l.user_id) AS active_user_total ")
                .append("FROM ods_chat_log l ")
                .append("LEFT JOIN (SELECT id, tenant_id FROM ods_chat_session FINAL) s ON s.id = l.session_id ")
                .append("WHERE l.created >= toDateTime(?) AND l.created < toDateTime(?)");
        args.add(CH_DATE_TIME_FORMATTER.format(startTime));
        args.add(CH_DATE_TIME_FORMATTER.format(endTime));
        appendOptionalTenantFilter(sql, args, tenantId, "s.tenant_id");
        sql.append(" GROUP BY bucket_key ORDER BY bucket_key ASC");

        return analyticalDBOperations.query(
                sql.toString(),
                (rs, rowNum) -> new ChatDashboardTrend(
                        rs.getString("bucket_key"),
                        rs.getLong("session_total"),
                        rs.getLong("message_total"),
                        rs.getLong("active_user_total")
                ),
                args.toArray()
        );
    }

    /**
     * 查询聊天助手使用排行。
     *
     * @param startDate 开始日期，包含
     * @param endDate 结束日期，不包含
     * @param tenantId 租户 ID，空表示全局
     * @param limit 返回条数
     * @return 排行列表
     */
    public List<ChatAssistantUsageRank> queryAssistantUsageRanks(LocalDate startDate,
                                                                 LocalDate endDate,
                                                                 BigInteger tenantId,
                                                                 int limit) {
        assertAvailable();
        int safeLimit = Math.max(limit, 1);
        List<Object> args = new java.util.ArrayList<>();
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT agg.assistant_id AS assistant_id, ")
                .append("snapshot.assistant_name AS assistant_name, ")
                .append("agg.user_total, agg.session_total, agg.message_total ")
                .append("FROM (")
                .append("SELECT agg.assistant_id, ")
                .append("uniqExact(agg.user_id) AS user_total, ")
                .append("uniqExact(agg.dimension_id) AS session_total, ")
                .append("ifNull(sum(agg.message_count), 0) AS message_total ")
                .append("FROM dws_chat_session_day agg ")
                .append("LEFT JOIN (SELECT id, tenant_id FROM ods_chat_session FINAL) s ON s.id = agg.dimension_id ")
                .append("WHERE agg.stat_date >= toDate(?) AND agg.stat_date < toDate(?)");
        args.add(startDate.toString());
        args.add(endDate.toString());
        appendOptionalTenantFilter(sql, args, tenantId, "s.tenant_id");
        sql.append(" GROUP BY agg.assistant_id")
                .append(") agg ")
                .append("LEFT JOIN (")
                .append("SELECT assistant_id, argMax(assistant_name, modified) AS assistant_name ")
                .append("FROM ods_chat_session FINAL WHERE is_deleted = 0 ");
        appendOptionalTenantFilter(sql, args, tenantId, "tenant_id");
        sql.append(" GROUP BY assistant_id")
                .append(") snapshot ON snapshot.assistant_id = agg.assistant_id ")
                .append("ORDER BY agg.session_total DESC, agg.user_total DESC, agg.message_total DESC, agg.assistant_id ASC ")
                .append("LIMIT ?");
        args.add(safeLimit);

        return analyticalDBOperations.query(
                sql.toString(),
                (rs, rowNum) -> new ChatAssistantUsageRank(
                        bigInteger(rs.getObject("assistant_id")),
                        rs.getString("assistant_name"),
                        rs.getLong("user_total"),
                        rs.getLong("session_total"),
                        rs.getLong("message_total")
                ),
                args.toArray()
        );
    }

    /**
     * 查询智能体日趋势。
     *
     * @param startDate 开始日期，包含
     * @param endDate 结束日期，不包含
     * @param tenantId 租户 ID，空表示全局
     * @param assistantIds 智能体 ID 列表
     * @return 趋势列表
     */
    public List<ChatAssistantSessionTrend> queryAssistantSessionTrends(LocalDate startDate,
                                                                       LocalDate endDate,
                                                                       BigInteger tenantId,
                                                                       List<BigInteger> assistantIds) {
        assertAvailable();
        if (assistantIds == null || assistantIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<Object> args = new ArrayList<>();
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT agg.assistant_id AS assistant_id, ")
                .append("snapshot.assistant_name AS assistant_name, ")
                .append("toString(agg.stat_date) AS bucket_key, ")
                .append("uniqExact(agg.dimension_id) AS session_total ")
                .append("FROM dws_chat_session_day agg ")
                .append("LEFT JOIN (SELECT id, tenant_id FROM ods_chat_session FINAL) s ON s.id = agg.dimension_id ")
                .append("LEFT JOIN (")
                .append("SELECT assistant_id, argMax(assistant_name, modified) AS assistant_name ")
                .append("FROM ods_chat_session FINAL WHERE is_deleted = 0 ");
        appendOptionalTenantFilter(sql, args, tenantId, "tenant_id");
        sql.append(" GROUP BY assistant_id")
                .append(") snapshot ON snapshot.assistant_id = agg.assistant_id ")
                .append("WHERE agg.stat_date >= toDate(?) AND agg.stat_date < toDate(?)");
        args.add(startDate.toString());
        args.add(endDate.toString());
        appendOptionalTenantFilter(sql, args, tenantId, "s.tenant_id");
        appendAssistantIdFilter(sql, args, "agg.assistant_id", assistantIds);
        sql.append(" GROUP BY agg.assistant_id, snapshot.assistant_name, agg.stat_date ")
                .append("ORDER BY agg.stat_date ASC, agg.assistant_id ASC");

        return analyticalDBOperations.query(
                sql.toString(),
                (rs, rowNum) -> new ChatAssistantSessionTrend(
                        bigInteger(rs.getObject("assistant_id")),
                        rs.getString("assistant_name"),
                        rs.getString("bucket_key"),
                        rs.getLong("session_total")
                ),
                args.toArray()
        );
    }

    /**
     * 查询智能体小时趋势。
     *
     * @param startTime 开始时间，包含
     * @param endTime 结束时间，不包含
     * @param tenantId 租户 ID，空表示全局
     * @param assistantIds 智能体 ID 列表
     * @return 趋势列表
     */
    public List<ChatAssistantSessionTrend> queryAssistantSessionHourlyTrends(LocalDateTime startTime,
                                                                             LocalDateTime endTime,
                                                                             BigInteger tenantId,
                                                                             List<BigInteger> assistantIds) {
        assertAvailable();
        if (assistantIds == null || assistantIds.isEmpty()) {
            return Collections.emptyList();
        }
        List<Object> args = new ArrayList<>();
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT l.assistant_id AS assistant_id, ")
                .append("snapshot.assistant_name AS assistant_name, ")
                .append("formatDateTime(toStartOfHour(l.created), '%Y-%m-%d %H:00:00') AS bucket_key, ")
                .append("uniqExact(l.session_id) AS session_total ")
                .append("FROM ods_chat_log l ")
                .append("LEFT JOIN (SELECT id, tenant_id FROM ods_chat_session FINAL) s ON s.id = l.session_id ")
                .append("LEFT JOIN (")
                .append("SELECT assistant_id, argMax(assistant_name, modified) AS assistant_name ")
                .append("FROM ods_chat_session FINAL WHERE is_deleted = 0 ");
        appendOptionalTenantFilter(sql, args, tenantId, "tenant_id");
        sql.append(" GROUP BY assistant_id")
                .append(") snapshot ON snapshot.assistant_id = l.assistant_id ")
                .append("WHERE l.created >= toDateTime(?) AND l.created < toDateTime(?)");
        args.add(CH_DATE_TIME_FORMATTER.format(startTime));
        args.add(CH_DATE_TIME_FORMATTER.format(endTime));
        appendOptionalTenantFilter(sql, args, tenantId, "s.tenant_id");
        appendAssistantIdFilter(sql, args, "l.assistant_id", assistantIds);
        sql.append(" GROUP BY l.assistant_id, snapshot.assistant_name, bucket_key ")
                .append("ORDER BY bucket_key ASC, l.assistant_id ASC");

        return analyticalDBOperations.query(
                sql.toString(),
                (rs, rowNum) -> new ChatAssistantSessionTrend(
                        bigInteger(rs.getObject("assistant_id")),
                        rs.getString("assistant_name"),
                        rs.getString("bucket_key"),
                        rs.getLong("session_total")
                ),
                args.toArray()
        );
    }

    /**
     * 查询活跃用户排行。
     *
     * @param startDate 开始日期，包含
     * @param endDate 结束日期，不包含
     * @param tenantId 租户 ID，空表示全局
     * @param assistantId 智能体 ID，空表示全部
     * @param limit 返回条数
     * @return 排行列表
     */
    public List<ChatActiveUserRank> queryActiveUserRanks(LocalDate startDate,
                                                         LocalDate endDate,
                                                         BigInteger tenantId,
                                                         BigInteger assistantId,
                                                         Integer limit) {
        assertAvailable();
        Integer safeLimit = limit == null ? null : Math.max(limit, 1);
        List<Object> args = new java.util.ArrayList<>();
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT agg.user_id AS user_id, ")
                .append("snapshot.user_account AS user_account, ")
                .append("agg.session_total, agg.message_total, agg.assistant_total ")
                .append("FROM (")
                .append("SELECT agg.user_id, ")
                .append("uniqExact(agg.dimension_id) AS session_total, ")
                .append("ifNull(sum(agg.message_count), 0) AS message_total, ")
                .append("uniqExact(agg.assistant_id) AS assistant_total ")
                .append("FROM dws_chat_session_day agg ")
                .append("LEFT JOIN (SELECT id, tenant_id FROM ods_chat_session FINAL) s ON s.id = agg.dimension_id ")
                .append("WHERE agg.stat_date >= toDate(?) AND agg.stat_date < toDate(?)");
        args.add(startDate.toString());
        args.add(endDate.toString());
        if (assistantId != null) {
            sql.append(" AND agg.assistant_id = ?");
            args.add(assistantId);
        }
        appendOptionalTenantFilter(sql, args, tenantId, "s.tenant_id");
        sql.append(" GROUP BY agg.user_id")
                .append(") agg ")
                .append("LEFT JOIN (")
                .append("SELECT user_id, argMax(user_account, modified) AS user_account ")
                .append("FROM ods_chat_session FINAL WHERE is_deleted = 0 ");
        appendOptionalTenantFilter(sql, args, tenantId, "tenant_id");
        sql.append(" GROUP BY user_id")
                .append(") snapshot ON snapshot.user_id = agg.user_id ")
                .append("ORDER BY agg.session_total DESC, agg.message_total DESC, agg.user_id ASC ");
        if (safeLimit != null) {
            sql.append("LIMIT ?");
            args.add(safeLimit);
        }

        return analyticalDBOperations.query(
                sql.toString(),
                (rs, rowNum) -> new ChatActiveUserRank(
                        bigInteger(rs.getObject("user_id")),
                        rs.getString("user_account"),
                        rs.getLong("session_total"),
                        rs.getLong("message_total"),
                        rs.getLong("assistant_total")
                ),
                args.toArray()
        );
    }

    /**
     * 追加智能体 ID 过滤。
     *
     * @param sql SQL 构造器
     * @param params 参数列表
     * @param columnName 列名
     * @param assistantIds 智能体 ID 列表
     */
    private void appendAssistantIdFilter(StringBuilder sql,
                                         List<Object> params,
                                         String columnName,
                                         List<BigInteger> assistantIds) {
        if (assistantIds == null || assistantIds.isEmpty()) {
            return;
        }
        List<BigInteger> nonNullAssistantIds = new ArrayList<>();
        boolean containsNullAssistantId = false;
        for (BigInteger assistantId : assistantIds) {
            if (assistantId == null) {
                containsNullAssistantId = true;
            } else {
                nonNullAssistantIds.add(assistantId);
            }
        }
        if (nonNullAssistantIds.isEmpty() && !containsNullAssistantId) {
            return;
        }

        sql.append(" AND (");
        boolean hasPreviousCondition = false;
        if (!nonNullAssistantIds.isEmpty()) {
            sql.append(columnName).append(" IN (");
            for (int i = 0; i < nonNullAssistantIds.size(); i++) {
                if (i > 0) {
                    sql.append(", ");
                }
                sql.append("?");
                params.add(nonNullAssistantIds.get(i));
            }
            sql.append(")");
            hasPreviousCondition = true;
        }
        if (containsNullAssistantId) {
            if (hasPreviousCondition) {
                sql.append(" OR ");
            }
            sql.append(columnName).append(" IS NULL");
        }
        sql.append(")");
    }

    public void refreshDws(Set<LocalDate> dates) {
        if (!enabled() || dates.isEmpty()) {
            return;
        }
        for (LocalDate date : dates) {
            String dateLiteral = date.toString();
            analyticalDBOperations.update("ALTER TABLE dws_chat_assistant_day DELETE WHERE stat_date = toDate(?)", dateLiteral);
            analyticalDBOperations.update("INSERT INTO dws_chat_assistant_day " +
                    "SELECT toDate(l.created) AS stat_date, " +
                    "l.assistant_id AS dimension_id, " +
                    "ifNull(s.tenant_id, toUInt64(0)) AS tenant_id, " +
                    "uniqExact(l.session_id) AS session_count, " +
                    "count() AS message_count " +
                    "FROM ods_chat_log l " +
                    "LEFT JOIN (SELECT id, tenant_id FROM ods_chat_session FINAL) s ON s.id = l.session_id " +
                    "WHERE toDate(l.created) = toDate(?) " +
                    "GROUP BY stat_date, dimension_id, tenant_id", dateLiteral);

            analyticalDBOperations.update("ALTER TABLE dws_chat_user_day DELETE WHERE stat_date = toDate(?)", dateLiteral);
            analyticalDBOperations.update("INSERT INTO dws_chat_user_day " +
                    "SELECT toDate(created) AS stat_date, user_id AS dimension_id, uniqExact(session_id) AS session_count, count() AS message_count " +
                    "FROM ods_chat_log WHERE toDate(created) = toDate(?) GROUP BY stat_date, dimension_id", dateLiteral);

            analyticalDBOperations.update("ALTER TABLE dws_chat_session_day DELETE WHERE stat_date = toDate(?)", dateLiteral);
            analyticalDBOperations.update("INSERT INTO dws_chat_session_day " +
                    "SELECT toDate(created) AS stat_date, session_id AS dimension_id, any(assistant_id) AS assistant_id, any(user_id) AS user_id, count() AS message_count " +
                    "FROM ods_chat_log WHERE toDate(created) = toDate(?) GROUP BY stat_date, dimension_id", dateLiteral);
        }
    }

    public List<String> findHistoryTables() {
        if (!enabled()) {
            return Collections.emptyList();
        }
        return analyticalDBOperations.query("SELECT name FROM system.tables WHERE database = currentDatabase()",
                (rs, rowNum) -> rs.getString(1));
    }

    private ParameterizedPreparedStatementSetter<ChatSessionSummary> sessionStatementSetter() {
        return (ps, summary) -> {
            ps.setObject(1, requiredUInt64(summary.getId()));
            ps.setObject(2, requiredUInt64(summary.getTenantId()));
            ps.setObject(3, requiredUInt64(summary.getDeptId()));
            ps.setObject(4, requiredUInt64(summary.getUserId()));
            ps.setString(5, requiredString(summary.getUserAccount()));
            ps.setObject(6, requiredUInt64(summary.getAssistantId()));
            ps.setString(7, requiredString(summary.getAssistantCode()));
            ps.setString(8, requiredString(summary.getAssistantName()));
            ps.setString(9, requiredString(summary.getTitle()));
            ps.setString(10, requiredString(summary.getLastMessagePreview()));
            ps.setObject(11, requiredUInt64(summary.getLastSenderId()));
            ps.setString(12, requiredString(summary.getLastSenderName()));
            ps.setInt(13, summary.getMessageCount() == null ? 0 : summary.getMessageCount());
            ps.setTimestamp(14, requiredTimestamp(summary.getAccessAt(), summary.getModified(), summary.getCreated()));
            ps.setTimestamp(15, timestamp(summary.getLastMessageAt()));
            ps.setTimestamp(16, requiredTimestamp(summary.getCreated(), summary.getAccessAt(), summary.getModified()));
            ps.setObject(17, requiredUInt64(summary.getCreatedBy()));
            ps.setTimestamp(18, requiredTimestamp(summary.getModified(), summary.getAccessAt(), summary.getCreated()));
            ps.setObject(19, requiredUInt64(summary.getModifiedBy()));
            ps.setInt(20, summary.getIsDeleted() == null ? 0 : summary.getIsDeleted());
        };
    }

    private ParameterizedPreparedStatementSetter<ChatMessageRecord> logStatementSetter() {
        return (ps, record) -> {
            ps.setObject(1, requiredUInt64(record.getId()));
            ps.setObject(2, requiredUInt64(record.getSessionId()));
            ps.setObject(3, requiredUInt64(record.getUserId()));
            ps.setObject(4, requiredUInt64(record.getAssistantId()));
            ps.setObject(5, requiredUInt64(record.getSenderId()));
            ps.setString(6, requiredString(record.getSenderName()));
            ps.setString(7, requiredString(record.getSenderRole()));
            ps.setString(8, requiredString(record.getContentType()));
            ps.setString(9, requiredString(record.getContentText()));
            ps.setString(10, requiredString(jsonSupport.toJson(record.getContentPayload())));
            ps.setTimestamp(11, requiredTimestamp(record.getCreated()));
            ps.setObject(12, requiredUInt64(record.getCreatedBy()));
            ps.setLong(13, record.getSyncVersion() == null ? System.currentTimeMillis() : record.getSyncVersion());
        };
    }

    private Timestamp timestamp(Date date) {
        return date == null ? null : new Timestamp(date.getTime());
    }

    private Timestamp requiredTimestamp(Date primary, Date... fallbacks) {
        Timestamp timestamp = timestamp(primary);
        if (timestamp != null) {
            return timestamp;
        }
        if (fallbacks != null) {
            for (Date fallback : fallbacks) {
                timestamp = timestamp(fallback);
                if (timestamp != null) {
                    return timestamp;
                }
            }
        }
        return new Timestamp(System.currentTimeMillis());
    }

    private BigInteger requiredUInt64(BigInteger value) {
        return value == null ? BigInteger.ZERO : value;
    }

    private String requiredString(String value) {
        return value == null ? "" : value;
    }

    private BigInteger bigInteger(Object value) {
        if (value == null) {
            return null;
        }
        return new BigInteger(String.valueOf(value));
    }

    /**
     * 追加可选租户过滤，超管场景可不传租户。
     *
     * @param sql SQL 构造器
     * @param args 参数列表
     * @param tenantId 租户 ID
     * @param columnName 列名
     */
    private void appendOptionalTenantFilter(StringBuilder sql, List<Object> args, BigInteger tenantId, String columnName) {
        if (tenantId == null || !StringUtils.hasText(columnName)) {
            return;
        }
        sql.append(" AND ").append(columnName).append(" = ?");
        args.add(tenantId);
    }

    private void appendSessionFilters(ChatSessionFilterQuery query, StringBuilder sql, List<Object> args) {
        if (query == null) {
            return;
        }
        if (StringUtils.hasText(query.getAssistantCode())) {
            sql.append(" AND assistant_code=?");
            args.add(query.getAssistantCode().trim());
        }
        if (query.getAssistantId() != null) {
            sql.append(" AND assistant_id=?");
            args.add(query.getAssistantId());
        }
        if (query.getUserId() != null) {
            sql.append(" AND user_id=?");
            args.add(query.getUserId());
        }
        if (query.getUserAccount() != null && !query.getUserAccount().isBlank()) {
            sql.append(" AND user_account LIKE ?");
            args.add("%" + query.getUserAccount().trim() + "%");
        }
        if (query.getStartTime() != null) {
            String startTime = formatDateTime(query.getStartTime());
            sql.append(" AND ((last_message_at IS NOT NULL AND last_message_at >= toDateTime(?)) OR (last_message_at IS NULL AND access_at >= toDateTime(?)))");
            args.add(startTime);
            args.add(startTime);
        }
        if (query.getEndTime() != null) {
            String endTime = formatDateTime(query.getEndTime());
            sql.append(" AND ((last_message_at IS NOT NULL AND last_message_at <= toDateTime(?)) OR (last_message_at IS NULL AND access_at <= toDateTime(?)))");
            args.add(endTime);
            args.add(endTime);
        }
    }

    private String formatDateTime(Date date) {
        if (date == null) {
            return null;
        }
        return CH_DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime());
    }
}
