package com.ycwl.basic.clickhouse.service.impl;

import com.ycwl.basic.clickhouse.service.StatsQueryService;
import com.ycwl.basic.mapper.TaskMapper;
import com.ycwl.basic.model.mobile.statistic.req.CommonQueryReq;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;

import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

/**
 * ClickHouse 统计数据查询服务实现
 * 当 clickhouse.enabled=true 时启用
 *
 * 注意：ClickHouse JDBC 驱动 0.6.x 对参数绑定支持有问题，
 * 因此使用字符串格式化方式构建 SQL（参数均为内部生成的数值或日期，无 SQL 注入风险）
 */
@Slf4j
@Service
@ConditionalOnProperty(prefix = "clickhouse", name = "enabled", havingValue = "true")
public class ClickHouseStatsQueryServiceImpl implements StatsQueryService {

    private static final TimeZone CLICKHOUSE_TIMEZONE = TimeZone.getTimeZone("Asia/Shanghai");

    /**
     * 创建日期格式化器（SimpleDateFormat 非线程安全，每次创建新实例）
     */
    private SimpleDateFormat createDateFormat() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        sdf.setTimeZone(CLICKHOUSE_TIMEZONE);
        return sdf;
    }

    /**
     * 创建日期时间格式化器
     */
    private SimpleDateFormat createDateTimeFormat() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdf.setTimeZone(CLICKHOUSE_TIMEZONE);
        return sdf;
    }

    @Autowired
    @Qualifier("clickHouseJdbcTemplate")
    private NamedParameterJdbcTemplate namedJdbcTemplate;

    private JdbcTemplate jdbcTemplate;

    @Autowired
    private TaskMapper taskMapper;

    private JdbcTemplate getJdbcTemplate() {
        if (jdbcTemplate == null) {
            jdbcTemplate = namedJdbcTemplate.getJdbcTemplate();
        }
        return jdbcTemplate;
    }

    /**
     * 格式化日期时间为 ClickHouse 可识别的字符串
     */
    private String formatDateTime(Date date) {
        return date != null ? "'" + createDateTimeFormat().format(date) + "'" : null;
    }

    /**
     * 格式化日期为 ClickHouse 可识别的字符串
     */
    private String formatDate(Date date) {
        return date != null ? "'" + createDateFormat().format(date) + "'" : null;
    }

    /**
     * 拼接“进入景区”的 trace_id 子查询。
     * <p>
     * ClickHouse 上 t_stats_record 往往按时间分区/排序；给子查询补充时间范围可显著减少扫描量。
     */
    private void appendEnterScenicTraceIdSubQuery(StringBuilder sql, Long scenicId, Date startTime, Date endTime) {
        sql.append("SELECT DISTINCT trace_id FROM t_stats_record ");
        sql.append("WHERE action = 'ENTER_SCENIC' ");
        if (scenicId != null) {
            sql.append("AND identifier = '").append(scenicId).append("' ");
        }
        if (startTime != null) {
            sql.append("AND create_time >= ").append(formatDateTime(startTime)).append(" ");
        }
        if (endTime != null) {
            sql.append("AND create_time <= ").append(formatDateTime(endTime)).append(" ");
        }
    }

    @Override
    public Integer countPreviewVideoOfMember(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.action = 'LOAD' ");
        sql.append("AND r.identifier = 'pages/videoSynthesis/buy' ");
        sql.append("AND r.trace_id IN (");
        appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
        sql.append(") ");
        sql.append("AND JSONExtractString(r.params, 'share') = '' ");
        if (query.getStartTime() != null) {
            sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
        }
        if (query.getEndTime() != null) {
            sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
        }

        return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
    }

    @Override
    public Integer countScanCodeOfMember(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.trace_id IN (");
        appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
        sql.append(") ");
        sql.append("AND r.action = 'LAUNCH' ");
        sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
        if (query.getStartTime() != null) {
            sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
        }
        if (query.getEndTime() != null) {
            sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
        }

        return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
    }

    @Override
    public Integer countPushOfMember(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.trace_id IN (");
        appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
        sql.append(") ");
        sql.append("AND r.action = 'PERM_REQ' ");
        sql.append("AND r.identifier = 'NOTIFY' ");
        if (query.getStartTime() != null) {
            sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
        }
        if (query.getEndTime() != null) {
            sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
        }

        return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
    }

    @Override
    public Integer countUploadFaceOfMember(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.action = 'FACE_UPLOAD' ");
        sql.append("AND r.trace_id IN (");
        appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
        sql.append(") ");
        if (query.getStartTime() != null) {
            sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
        }
        if (query.getEndTime() != null) {
            sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
        }

        return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
    }

    private List<String> listFaceIdsWithUpload(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT DISTINCT r.identifier FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.action = 'FACE_UPLOAD' ");
        sql.append("AND r.trace_id IN (");
        appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
        sql.append(") ");
        if (query.getStartTime() != null) {
            sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
        }
        if (query.getEndTime() != null) {
            sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
        }

        return getJdbcTemplate().queryForList(sql.toString(), String.class);
    }

    @Override
    public Integer countCompleteVideoOfMember(CommonQueryReq query) {
        List<String> faceIds = listFaceIdsWithUpload(query);
        if (faceIds == null || faceIds.isEmpty()) {
            return 0;
        }
        return taskMapper.countCompletedTaskMembersByFaceIds(faceIds);
    }

    @Override
    public Integer countCompleteOfVideo(CommonQueryReq query) {
        List<String> faceIds = listFaceIdsWithUpload(query);
        if (faceIds == null || faceIds.isEmpty()) {
            return 0;
        }
        return taskMapper.countCompletedTasksByFaceIds(faceIds);
    }

    @Override
    public Integer countTotalVisitorOfMember(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT toInt32(uniqExact(s.member_id)) AS count ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.trace_id IN (");
        appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
        sql.append(") ");
        sql.append("AND r.action = 'LAUNCH' ");
        if (query.getStartTime() != null) {
            sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
        }
        if (query.getEndTime() != null) {
            sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
        }

        return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
    }

    @Override
    public Integer countPreviewOfVideo(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("WITH JSONExtractString(params, 'id') AS videoId, ");
        sql.append("     JSONExtractString(params, 'share') AS share ");
        sql.append("SELECT toInt32(uniqExact(videoId)) AS count ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.trace_id IN (");
        appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
        sql.append(") ");
        sql.append("AND r.action = 'LOAD' ");
        sql.append("AND r.identifier = 'pages/videoSynthesis/buy' ");
        sql.append("AND videoId != '' ");
        sql.append("AND share = '' ");
        if (query.getStartTime() != null) {
            sql.append("AND s.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
        }
        if (query.getEndTime() != null) {
            sql.append("AND s.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
        }

        return getJdbcTemplate().queryForObject(sql.toString(), Integer.class);
    }

    @Override
    public List<Long> getBrokerIdListForUser(Long memberId, Date startTime, Date endTime) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT toInt64(r.identifier) AS identifier ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.action = 'CODE_SCAN' ");
        sql.append("  AND s.member_id = ").append(memberId).append(" ");
        if (startTime != null) {
            sql.append("  AND r.create_time >= ").append(formatDateTime(startTime)).append(" ");
        }
        if (endTime != null) {
            sql.append("  AND r.create_time <= ").append(formatDateTime(endTime)).append(" ");
        }
        sql.append("GROUP BY r.identifier ");
        sql.append("ORDER BY max(r.create_time) DESC");

        return getJdbcTemplate().queryForList(sql.toString(), Long.class);
    }

    @Override
    public Long getUserRecentEnterType(Long memberId, Date endTime) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT JSONExtractInt(r.params, 'scene') AS scene ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.action = 'LAUNCH' ");
        sql.append("  AND s.member_id = ").append(memberId).append(" ");
        if (endTime != null) {
            sql.append("  AND r.create_time <= ").append(formatDateTime(endTime)).append(" ");
        }
        sql.append("ORDER BY r.create_time DESC LIMIT 1");

        try {
            return getJdbcTemplate().queryForObject(sql.toString(), Long.class);
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public List<Long> getProjectIdListForUser(Long memberId, Date startTime, Date endTime) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT toInt64(r.identifier) AS identifier ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE s.member_id = ").append(memberId).append(" ");
        sql.append("  AND r.action = 'ENTER_PROJECT' ");
        sql.append("  AND r.create_time < ").append(formatDateTime(endTime)).append(" ");
        sql.append("  AND r.create_time > ").append(formatDateTime(startTime)).append(" ");
        sql.append("ORDER BY r.create_time DESC LIMIT 1");

        return getJdbcTemplate().queryForList(sql.toString(), Long.class);
    }

    @Override
    public Integer countBrokerScanCount(Long brokerId) {
        String sql = "SELECT count(1) AS count FROM t_stats_record " +
                     "WHERE action = 'CODE_SCAN' AND identifier = '" + brokerId + "'";

        return getJdbcTemplate().queryForObject(sql, Integer.class);
    }

    @Override
    public List<HashMap<String, Object>> getDailyScanStats(Long brokerId, Date startTime, Date endTime) {
        SimpleDateFormat dateFormat = createDateFormat();
        String startDateStr = dateFormat.format(startTime);
        String endDateStr = dateFormat.format(endTime);
        String startDateTimeStr = "'" + startDateStr + " 00:00:00'";
        String endDateTimeStr = "'" + endDateStr + " 23:59:59'";

        String sql = "SELECT toDate(create_time) AS date, count(DISTINCT id) AS scanCount " +
                     "FROM t_stats_record " +
                     "WHERE action = 'CODE_SCAN' " +
                     "  AND identifier = '" + brokerId + "' " +
                     "  AND create_time >= " + startDateTimeStr + " " +
                     "  AND create_time <= " + endDateTimeStr + " " +
                     "GROUP BY toDate(create_time) " +
                     "ORDER BY toDate(create_time)";

        return getJdbcTemplate().query(sql, (rs, rowNum) -> {
            HashMap<String, Object> map = new HashMap<>();
            map.put("date", rs.getDate("date"));
            map.put("scanCount", rs.getLong("scanCount"));
            return map;
        });
    }

    @Override
    public List<HashMap<String, String>> scanCodeMemberChartByHour(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT formatDateTime(toStartOfHour(s.create_time), '%m-%d %H') AS t, ");
        sql.append("       uniqExact(s.member_id) AS count ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.trace_id IN (");
        appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
        sql.append(") ");
        sql.append("AND r.action = 'LAUNCH' ");
        sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
        sql.append("GROUP BY toStartOfHour(s.create_time) ");
        sql.append("ORDER BY toStartOfHour(s.create_time)");

        List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
            HashMap<String, String> map = new HashMap<>();
            map.put("t", rs.getString("t"));
            map.put("count", rs.getString("count"));
            return map;
        });

        return fillHourSeries(rawData, query.getStartTime(), query.getEndTime());
    }

    @Override
    public List<HashMap<String, String>> scanCodeMemberChartByDate(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT formatDateTime(toStartOfDay(s.create_time), '%m-%d') AS t, ");
        sql.append("       uniqExact(s.member_id) AS count ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.trace_id IN (");
        appendEnterScenicTraceIdSubQuery(sql, query.getScenicId(), query.getStartTime(), query.getEndTime());
        sql.append(") ");
        sql.append("AND r.action = 'LAUNCH' ");
        sql.append("AND JSONExtractInt(r.params, 'scene') IN (1047, 1048, 1049) ");
        sql.append("GROUP BY toStartOfDay(s.create_time) ");
        sql.append("ORDER BY toStartOfDay(s.create_time)");

        List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
            HashMap<String, String> map = new HashMap<>();
            map.put("t", rs.getString("t"));
            map.put("count", rs.getString("count"));
            return map;
        });

        return fillDateSeries(rawData, query.getStartTime(), query.getEndTime());
    }

    @Override
    public List<HashMap<String, String>> printerFromSampleChartByHour(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT formatDateTime(toStartOfHour(s.create_time), '%m-%d %H') AS t, ");
        sql.append("       uniqExact(s.member_id) AS count ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.action = 'LOAD' ");
        sql.append("AND r.identifier = 'pages/printer/hello' ");
        if (query.getScenicId() != null) {
            sql.append("AND JSONExtractString(r.params, 'scenicId') = '").append(query.getScenicId()).append("' ");
        }
        if (query.getStartTime() != null) {
            sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
        }
        if (query.getEndTime() != null) {
            sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
        }
        sql.append("GROUP BY toStartOfHour(s.create_time) ");
        sql.append("ORDER BY toStartOfHour(s.create_time)");

        List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
            HashMap<String, String> map = new HashMap<>();
            map.put("t", rs.getString("t"));
            map.put("count", rs.getString("count"));
            return map;
        });

        return fillHourSeries(rawData, query.getStartTime(), query.getEndTime());
    }

    @Override
    public List<HashMap<String, String>> printerFromSampleChartByDate(CommonQueryReq query) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT formatDateTime(toStartOfDay(s.create_time), '%m-%d') AS t, ");
        sql.append("       uniqExact(s.member_id) AS count ");
        sql.append("FROM t_stats_record r ");
        sql.append("INNER JOIN t_stats s ON r.trace_id = s.trace_id ");
        sql.append("WHERE r.action = 'LOAD' ");
        sql.append("AND r.identifier = 'pages/printer/hello' ");
        if (query.getScenicId() != null) {
            sql.append("AND JSONExtractString(r.params, 'scenicId') = '").append(query.getScenicId()).append("' ");
        }
        if (query.getStartTime() != null) {
            sql.append("AND r.create_time >= ").append(formatDateTime(query.getStartTime())).append(" ");
        }
        if (query.getEndTime() != null) {
            sql.append("AND r.create_time <= ").append(formatDateTime(query.getEndTime())).append(" ");
        }
        sql.append("GROUP BY toStartOfDay(s.create_time) ");
        sql.append("ORDER BY toStartOfDay(s.create_time)");

        List<HashMap<String, String>> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> {
            HashMap<String, String> map = new HashMap<>();
            map.put("t", rs.getString("t"));
            map.put("count", rs.getString("count"));
            return map;
        });

        return fillDateSeries(rawData, query.getStartTime(), query.getEndTime());
    }

    /**
     * 填充小时序列，确保每个小时都有数据（缺失的填充为0）
     */
    private List<HashMap<String, String>> fillHourSeries(List<HashMap<String, String>> rawData, Date startTime, Date endTime) {
        if (startTime == null || endTime == null) {
            return rawData;
        }

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd HH");
        LocalDateTime start = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().truncatedTo(ChronoUnit.HOURS);
        LocalDateTime end = endTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().truncatedTo(ChronoUnit.HOURS);

        // 将原始数据转为 Map 以便快速查找
        Map<String, String> dataMap = rawData.stream()
                .collect(Collectors.toMap(
                        m -> m.get("t"),
                        m -> m.get("count"),
                        (existing, replacement) -> existing
                ));

        List<HashMap<String, String>> result = new ArrayList<>();
        LocalDateTime current = start;
        while (!current.isAfter(end)) {
            String timeKey = current.format(formatter);
            HashMap<String, String> item = new HashMap<>();
            item.put("t", timeKey);
            item.put("count", dataMap.getOrDefault(timeKey, "0"));
            result.add(item);
            current = current.plusHours(1);
        }

        return result;
    }

    /**
     * 填充日期序列，确保每天都有数据（缺失的填充为0）
     */
    private List<HashMap<String, String>> fillDateSeries(List<HashMap<String, String>> rawData, Date startTime, Date endTime) {
        if (startTime == null || endTime == null) {
            return rawData;
        }

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
        LocalDate start = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate end = endTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

        // 将原始数据转为 Map 以便快速查找
        Map<String, String> dataMap = rawData.stream()
                .collect(Collectors.toMap(
                        m -> m.get("t"),
                        m -> m.get("count"),
                        (existing, replacement) -> existing
                ));

        List<HashMap<String, String>> result = new ArrayList<>();
        LocalDate current = start;
        while (!current.isAfter(end)) {
            String timeKey = current.format(formatter);
            HashMap<String, String> item = new HashMap<>();
            item.put("t", timeKey);
            item.put("count", dataMap.getOrDefault(timeKey, "0"));
            result.add(item);
            current = current.plusDays(1);
        }

        return result;
    }
}
