package com.xzzn.ems.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.xzzn.common.exception.ServiceException;
import com.xzzn.common.constant.RedisKeyConstants;
import com.xzzn.common.core.redis.RedisCache;
import com.xzzn.common.utils.DateUtils;
import com.xzzn.common.utils.StringUtils;
import com.xzzn.ems.domain.EmsPointConfig;
import com.xzzn.ems.domain.vo.ImportPointTemplateRequest;
import com.xzzn.ems.domain.vo.PointConfigCurveRequest;
import com.xzzn.ems.domain.vo.PointConfigCurveValueVo;
import com.xzzn.ems.domain.vo.PointConfigGenerateRecentRequest;
import com.xzzn.ems.domain.vo.PointConfigLatestValueItemVo;
import com.xzzn.ems.domain.vo.PointConfigLatestValueRequest;
import com.xzzn.ems.domain.vo.PointConfigLatestValueVo;
import com.xzzn.ems.mapper.EmsPointConfigMapper;
import com.xzzn.ems.service.IEmsPointConfigService;
import com.xzzn.ems.service.InfluxPointDataWriter;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
public class EmsPointConfigServiceImpl implements IEmsPointConfigService {
    private static final Logger log = LoggerFactory.getLogger(EmsPointConfigServiceImpl.class);
    private static final String TEMPLATE_SITE_ID = "DEFAULT";
    private static final int CSV_IMPORT_BATCH_SIZE = 200;
    private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[A-Za-z0-9_]+$");
    private static final Pattern INTEGER_PATTERN = Pattern.compile("^-?\\d+$");
    private static final Pattern DECIMAL_PATTERN = Pattern.compile("^-?\\d+(\\.\\d+)?$");
    private static final Pattern CALC_EXPRESSION_PATTERN = Pattern.compile("^[0-9A-Za-z_+\\-*/().,?:<>=!&|\\s]+$");
    private static final Pattern AUTO_DAY_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)DAY_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)");
    private static final Pattern AUTO_MONTH_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)MONTH_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)");
    private static final Pattern AUTO_HOUR_DIFF_FUNCTION_PATTERN = Pattern.compile("(?i)HOUR_DIFF\\s*\\(\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\)");
    private static final String AUTO_DAY_DIFF_CONTEXT_PREFIX = "__AUTO_DAY_DIFF__";
    private static final String AUTO_MONTH_DIFF_CONTEXT_PREFIX = "__AUTO_MONTH_DIFF__";
    private static final String AUTO_HOUR_DIFF_CONTEXT_PREFIX = "__AUTO_HOUR_DIFF__";
    private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Autowired
    private EmsPointConfigMapper emsPointConfigMapper;
    @Autowired
    private RedisCache redisCache;
    @Autowired
    private InfluxPointDataWriter influxPointDataWriter;

    @Override
    public List<EmsPointConfig> selectPointConfigList(EmsPointConfig pointConfig) {
        return emsPointConfigMapper.selectEmsPointConfigList(pointConfig);
    }

    @Override
    public EmsPointConfig selectPointConfigById(Long id) {
        return emsPointConfigMapper.selectEmsPointConfigById(id);
    }

    @Override
    public int insertPointConfig(EmsPointConfig pointConfig, String operName) {
        if (pointConfig == null || StringUtils.isBlank(pointConfig.getSiteId())) {
            throw new ServiceException("站点ID不能为空");
        }
        normalizeAndValidatePointConfig(pointConfig);
        applyCollectDefaultsForInsert(pointConfig);
        pointConfig.setCreateBy(operName);
        pointConfig.setUpdateBy(operName);
        int rows;
        try {
            rows = emsPointConfigMapper.insertEmsPointConfig(pointConfig);
        } catch (Exception ex) {
            throw translatePointConfigPersistenceException(ex, pointConfig == null ? null : pointConfig.getPointId());
        }
        if (rows > 0) {
            invalidatePointConfigCache(pointConfig.getSiteId(), pointConfig.getDeviceId());
        }
        return rows;
    }

    @Override
    public int updatePointConfig(EmsPointConfig pointConfig, String operName) {
        EmsPointConfig oldConfig = pointConfig.getId() == null ? null : emsPointConfigMapper.selectEmsPointConfigById(pointConfig.getId());
        normalizeAndValidatePointConfig(pointConfig);
        pointConfig.setUpdateBy(operName);
        int rows;
        try {
            rows = emsPointConfigMapper.updateEmsPointConfig(pointConfig);
        } catch (Exception ex) {
            throw translatePointConfigPersistenceException(ex, pointConfig == null ? null : pointConfig.getPointId());
        }
        if (rows > 0) {
            if (oldConfig != null) {
                invalidatePointConfigCache(oldConfig.getSiteId(), oldConfig.getDeviceId());
            }
            invalidatePointConfigCache(pointConfig.getSiteId(), pointConfig.getDeviceId());
        }
        return rows;
    }

    @Override
    public int deletePointConfigByIds(Long[] ids) {
        if (ids != null) {
            for (Long id : ids) {
                EmsPointConfig oldConfig = emsPointConfigMapper.selectEmsPointConfigById(id);
                if (oldConfig != null) {
                    invalidatePointConfigCache(oldConfig.getSiteId(), oldConfig.getDeviceId());
                }
            }
        }
        return emsPointConfigMapper.deleteEmsPointConfigByIds(ids);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String importTemplateBySite(ImportPointTemplateRequest request, String operName) {
        String targetSiteId = request.getSiteId();
        if (StringUtils.isBlank(targetSiteId)) {
            throw new ServiceException("站点ID不能为空");
        }
        if (TEMPLATE_SITE_ID.equals(targetSiteId)) {
            throw new ServiceException("模板站点不支持作为导入目标站点");
        }

        int templateCount = emsPointConfigMapper.countBySiteId(TEMPLATE_SITE_ID);
        if (templateCount <= 0) {
            throw new ServiceException("模板点位配置不存在，无法导入");
        }

        boolean overwrite = Boolean.TRUE.equals(request.getOverwrite());
        int targetCount = emsPointConfigMapper.countBySiteId(targetSiteId);
        if (targetCount > 0 && !overwrite) {
            throw new ServiceException("目标站点已存在点位配置，请勾选“覆盖已存在数据”后重试");
        }

        if (targetCount > 0) {
            emsPointConfigMapper.deleteBySiteId(targetSiteId);
        }

        int importCount = emsPointConfigMapper.copyTemplateToSite(TEMPLATE_SITE_ID, targetSiteId, operName);
        invalidatePointConfigCacheBySite(targetSiteId);
        return String.format("导入成功：站点 %s，点位 %d 条", targetSiteId, importCount);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String importCsvBySite(String siteId, Boolean overwrite, MultipartFile file, String operName) {
        if (StringUtils.isBlank(siteId)) {
            throw new ServiceException("站点ID不能为空");
        }
        if (!DB_NAME_PATTERN.matcher(siteId).matches()) {
            throw new ServiceException(String.format("站点ID不合法（siteId=%s）", siteId));
        }
        if (TEMPLATE_SITE_ID.equals(siteId)) {
            throw new ServiceException("模板站点不支持作为导入目标站点");
        }
        if (file == null || file.isEmpty()) {
            throw new ServiceException("请上传CSV文件");
        }
        if (!StringUtils.endsWithIgnoreCase(file.getOriginalFilename(), ".csv")) {
            throw new ServiceException("仅支持上传CSV文件");
        }

        List<EmsPointConfig> pointConfigList = parseCsv(file, siteId);
        if (pointConfigList.isEmpty()) {
            throw new ServiceException("CSV没有可导入的数据");
        }

        List<String> pointIds = pointConfigList.stream()
                .map(EmsPointConfig::getPointId)
                .filter(StringUtils::isNotBlank)
                .distinct()
                .collect(Collectors.toList());
        Map<String, EmsPointConfig> existingPointMap = new HashMap<>();
        if (!pointIds.isEmpty()) {
            List<EmsPointConfig> existingList = emsPointConfigMapper.selectBySiteIdAndPointIds(siteId, pointIds);
            if (CollectionUtils.isNotEmpty(existingList)) {
                for (EmsPointConfig existing : existingList) {
                    if (existing != null && StringUtils.isNotBlank(existing.getPointId())) {
                        existingPointMap.put(existing.getPointId(), existing);
                    }
                }
            }
        }

        List<EmsPointConfig> insertList = new ArrayList<>();
        int updateCount = 0;
        int insertCount = 0;
        long importStart = System.currentTimeMillis();
        for (EmsPointConfig pointConfig : pointConfigList) {
            EmsPointConfig existing = existingPointMap.get(pointConfig.getPointId());
            if (existing == null) {
                pointConfig.setCreateBy(operName);
                pointConfig.setUpdateBy(operName);
                insertList.add(pointConfig);
                continue;
            }
            pointConfig.setId(existing.getId());
            pointConfig.setUpdateBy(operName);
            try {
                updateCount += emsPointConfigMapper.updateEmsPointConfigForImport(pointConfig);
            } catch (Exception ex) {
                throw translatePointConfigPersistenceException(ex, pointConfig.getPointId());
            }
        }

        for (int fromIndex = 0; fromIndex < insertList.size(); fromIndex += CSV_IMPORT_BATCH_SIZE) {
            int toIndex = Math.min(fromIndex + CSV_IMPORT_BATCH_SIZE, insertList.size());
            List<EmsPointConfig> batchList = insertList.subList(fromIndex, toIndex);
            long batchStart = System.currentTimeMillis();
            try {
                insertCount += emsPointConfigMapper.insertBatchEmsPointConfig(batchList);
            } catch (Exception ex) {
                throw translatePointConfigPersistenceException(ex, null);
            }
            log.info("点位CSV导入批量新增完成，siteId={}, batch={}~{}, batchSize={}, costMs={}",
                    siteId, fromIndex + 1, toIndex, batchList.size(), System.currentTimeMillis() - batchStart);
        }
        int importCount = updateCount + insertCount;
        log.info("点位CSV导入完成，siteId={}, total={}, insert={}, update={}, costMs={}",
                siteId, importCount, insertCount, updateCount, System.currentTimeMillis() - importStart);

        invalidatePointConfigCacheBySite(siteId);
        return String.format("导入成功：站点 %s，新增 %d 条，更新 %d 条", siteId, insertCount, updateCount);
    }

    @Override
    public String getRegisterAddress(String siteId, String deviceCategory, String deviceId, String dataKey) {
        if (StringUtils.isAnyBlank(siteId, deviceCategory, deviceId, dataKey)) {
            return null;
        }
        return emsPointConfigMapper.getRegisterAddress(siteId, deviceCategory, deviceId, dataKey);
    }

    @Override
    public List<PointConfigLatestValueVo> getLatestValues(PointConfigLatestValueRequest request) {
        List<PointConfigLatestValueVo> result = new ArrayList<>();
        if (request == null || request.getPoints() == null || request.getPoints().isEmpty()) {
            log.warn("latestValues 查询参数为空，request: {}", request);
            return result;
        }
        log.info("latestValues 开始查询，pointCount: {}", request.getPoints().size());
        for (PointConfigLatestValueItemVo item : request.getPoints()) {
            if (item == null || StringUtils.isBlank(item.getPointId())) {
                log.warn("latestValues 跳过空点位项，item: {}", item);
                continue;
            }
            PointConfigLatestValueVo latestValue = queryLatestValueByPointIdFromRedis(item);
            result.add(latestValue);
        }
        long hitCount = result.stream().filter(vo -> vo.getPointValue() != null).count();
        log.info("latestValues 查询结束，total: {}, hit: {}, miss: {}",
                result.size(), hitCount, result.size() - hitCount);
        return result;
    }

    @Override
    public List<PointConfigCurveValueVo> getCurveData(PointConfigCurveRequest request) {
        if (request == null || StringUtils.isBlank(request.getSiteId())) {
            return new ArrayList<>();
        }
        String siteId = StringUtils.trim(request.getSiteId());
        String pointId = StringUtils.trim(request.getPointId());
        if (StringUtils.isAnyBlank(siteId, pointId)) {
            return new ArrayList<>();
        }
        Date[] range = resolveTimeRange(request);
        return queryCurveDataFromInflux(siteId, pointId, range[0], range[1]);
    }

    @Override
    public String generateRecent7DaysData(PointConfigGenerateRecentRequest request) {
        if (request == null || StringUtils.isAnyBlank(request.getSiteId(), request.getPointId())) {
            throw new ServiceException("站点ID和点位ID不能为空");
        }
        String siteId = StringUtils.trim(request.getSiteId());
        String pointId = StringUtils.trim(request.getPointId());
        String deviceId = StringUtils.isBlank(request.getDeviceId()) ? "SITE_CALC" : StringUtils.trim(request.getDeviceId());

        EmsPointConfig targetConfig = selectPointConfigByExactPointId(siteId, pointId);
        if (targetConfig == null || !"calc".equalsIgnoreCase(StringUtils.defaultString(targetConfig.getPointType()))) {
            throw new ServiceException("仅计算点支持按表达式生成最近7天数据");
        }
        String expression = StringUtils.trimToNull(targetConfig.getCalcExpression());
        if (StringUtils.isBlank(expression)) {
            throw new ServiceException("计算点缺少表达式，无法生成数据");
        }

        Integer decimalScale = targetConfig.getDecimalScale();
        List<InfluxPointDataWriter.PointWritePayload> payloads = buildCalcRecentPayloads(siteId, deviceId, pointId, expression, decimalScale);
        if (payloads.isEmpty()) {
            throw new ServiceException("未生成有效数据，请检查表达式依赖点是否有历史数据");
        }
        influxPointDataWriter.writeBatch(payloads);
        return String.format("已生成点位 %s 最近7天数据，共 %d 条", pointId, payloads.size());
    }

    private List<InfluxPointDataWriter.PointWritePayload> buildCalcRecentPayloads(String siteId, String deviceId, String pointId, String expression, Integer decimalScale) {
        CompiledExpression compiledExpression = new CompiledExpression(expression);
        List<LocalDateTime> evaluateTimes = buildEvaluateTimes(expression);
        if (evaluateTimes.isEmpty()) {
            return Collections.emptyList();
        }
        EvaluateGranularity granularity = resolveEvaluateGranularity(expression);
        LocalDateTime start = evaluateTimes.get(0);
        LocalDateTime end = evaluateTimes.get(evaluateTimes.size() - 1);

        Set<String> variables = extractExpressionVariables(expression);
        Map<String, List<InfluxPointDataWriter.PointValue>> variableSeriesMap = loadVariableSeries(siteId, variables, start.minusDays(35), end);
        Map<String, PeriodDiffState> periodDiffStateMap = new HashMap<>();
        initializeRecentPeriodDiffState(expression, evaluateTimes, variableSeriesMap, periodDiffStateMap);
        List<InfluxPointDataWriter.PointWritePayload> payloads = new ArrayList<>(evaluateTimes.size());
        for (LocalDateTime evaluateTime : evaluateTimes) {
            Map<String, BigDecimal> contextValues = buildContextValuesByTime(evaluateTime, variableSeriesMap, granularity);
            if (contextValues.isEmpty()) {
                continue;
            }
            BigDecimal value;
            try {
                prepareAutoPeriodDiffContextValues(expression, contextValues, evaluateTime, periodDiffStateMap);
                value = compiledExpression.evaluate(contextValues).asNumber();
                int scale = (decimalScale != null && decimalScale >= 0) ? decimalScale : 4;
                value = value.setScale(scale, RoundingMode.HALF_UP);
            } catch (IllegalArgumentException ex) {
                continue;
            }
            payloads.add(new InfluxPointDataWriter.PointWritePayload(
                    siteId,
                    deviceId,
                    pointId,
                    value,
                    Timestamp.valueOf(evaluateTime)
            ));
        }
        return payloads;
    }

    private void initializeRecentPeriodDiffState(String expression,
                                                 List<LocalDateTime> evaluateTimes,
                                                 Map<String, List<InfluxPointDataWriter.PointValue>> variableSeriesMap,
                                                 Map<String, PeriodDiffState> periodDiffStateMap) {
        if (StringUtils.isBlank(expression)
                || CollectionUtils.isEmpty(evaluateTimes)
                || variableSeriesMap == null
                || periodDiffStateMap == null) {
            return;
        }
        LocalDateTime firstEvaluateTime = evaluateTimes.get(0);
        initializeSingleRecentDiffState("DAY_DIFF", expression, AUTO_DAY_DIFF_FUNCTION_PATTERN,
                "yyyy-MM-dd", firstEvaluateTime, variableSeriesMap, periodDiffStateMap);
        initializeSingleRecentDiffState("MONTH_DIFF", expression, AUTO_MONTH_DIFF_FUNCTION_PATTERN,
                "yyyy-MM", firstEvaluateTime, variableSeriesMap, periodDiffStateMap);
        initializeSingleRecentDiffState("HOUR_DIFF", expression, AUTO_HOUR_DIFF_FUNCTION_PATTERN,
                "yyyy-MM-dd HH", firstEvaluateTime, variableSeriesMap, periodDiffStateMap);
    }

    private void initializeSingleRecentDiffState(String functionName,
                                                 String expression,
                                                 Pattern functionPattern,
                                                 String periodFormat,
                                                 LocalDateTime firstEvaluateTime,
                                                 Map<String, List<InfluxPointDataWriter.PointValue>> variableSeriesMap,
                                                 Map<String, PeriodDiffState> periodDiffStateMap) {
        Set<String> variables = extractAutoDiffVariables(expression, functionPattern);
        if (variables.isEmpty()) {
            return;
        }
        long previousMillis = Timestamp.valueOf(firstEvaluateTime).getTime() - 1;
        if (previousMillis < Long.MIN_VALUE + 1) {
            return;
        }
        String previousPeriod = DateUtils.parseDateToStr(periodFormat, new Date(previousMillis));
        for (String variable : variables) {
            List<InfluxPointDataWriter.PointValue> values = variableSeriesMap.get(variable);
            InfluxPointDataWriter.PointValue previous = findLatestInWindow(values, Long.MIN_VALUE, previousMillis);
            if (previous == null || previous.getPointValue() == null) {
                continue;
            }
            String stateKey = functionName.toUpperCase(Locale.ROOT) + "_" + variable.toUpperCase(Locale.ROOT);
            PeriodDiffState state = periodDiffStateMap.computeIfAbsent(stateKey, k -> new PeriodDiffState());
            state.period = previousPeriod;
            state.lastValue = previous.getPointValue();
        }
    }

    private List<LocalDateTime> buildEvaluateTimes(String expression) {
        LocalDateTime now = LocalDateTime.now();
        List<LocalDateTime> times = new ArrayList<>();
        EvaluateGranularity granularity = resolveEvaluateGranularity(expression);
        boolean containsHourDiff = granularity == EvaluateGranularity.HOUR;
        boolean containsMonthDiff = granularity == EvaluateGranularity.MONTH;
        if (containsHourDiff) {
            int totalHours = 24 * 7;
            LocalDateTime latestCompletedHourEnd = now.withMinute(0).withSecond(0).withNano(0).minusSeconds(1);
            for (int i = totalHours - 1; i >= 0; i--) {
                times.add(latestCompletedHourEnd.minusHours(i));
            }
            return times;
        }
        if (containsMonthDiff) {
            times.add(now.withSecond(0).withNano(0));
            return times;
        }
        for (int i = 6; i >= 0; i--) {
            LocalDateTime dayTime = now.minusDays(i).withHour(23).withMinute(59).withSecond(59).withNano(0);
            if (dayTime.isAfter(now)) {
                dayTime = now.withNano(0);
            }
            times.add(dayTime);
        }
        return times;
    }

    private EvaluateGranularity resolveEvaluateGranularity(String expression) {
        if (AUTO_HOUR_DIFF_FUNCTION_PATTERN.matcher(expression).find()) {
            return EvaluateGranularity.HOUR;
        }
        if (AUTO_MONTH_DIFF_FUNCTION_PATTERN.matcher(expression).find()) {
            return EvaluateGranularity.MONTH;
        }
        return EvaluateGranularity.DAY;
    }

    private Set<String> extractExpressionVariables(String expression) {
        Set<String> reserved = new HashSet<>();
        reserved.add("IF");
        reserved.add("DAY_DIFF");
        reserved.add("MONTH_DIFF");
        reserved.add("HOUR_DIFF");
        Set<String> variables = new HashSet<>();
        for (ExprToken token : tokenizeExpression(expression)) {
            if (token.type == ExprTokenType.IDENTIFIER) {
                String identifier = StringUtils.defaultString(token.text).trim().toUpperCase(Locale.ROOT);
                if (StringUtils.isNotBlank(identifier) && !reserved.contains(identifier)) {
                    variables.add(identifier);
                }
            }
        }
        return variables;
    }

    private Map<String, List<InfluxPointDataWriter.PointValue>> loadVariableSeries(String siteId,
                                                                                    Set<String> variables,
                                                                                    LocalDateTime start,
                                                                                    LocalDateTime end) {
        Map<String, List<InfluxPointDataWriter.PointValue>> variableSeriesMap = new HashMap<>();
        if (variables == null || variables.isEmpty()) {
            return variableSeriesMap;
        }
        Date startDate = Timestamp.valueOf(start);
        Date endDate = Timestamp.valueOf(end);
        for (String variable : variables) {
            List<InfluxPointDataWriter.PointValue> values = influxPointDataWriter.queryCurveDataByPointKey(siteId, variable, startDate, endDate);
            if (values == null) {
                values = new ArrayList<>();
            }
            values.sort(Comparator.comparing(InfluxPointDataWriter.PointValue::getDataTime, Comparator.nullsLast(Date::compareTo)));
            variableSeriesMap.put(variable, values);
        }
        return variableSeriesMap;
    }

    private Map<String, BigDecimal> buildContextValuesByTime(LocalDateTime evaluateTime,
                                                             Map<String, List<InfluxPointDataWriter.PointValue>> variableSeriesMap,
                                                             EvaluateGranularity granularity) {
        Map<String, BigDecimal> context = new HashMap<>();
        if (variableSeriesMap == null || variableSeriesMap.isEmpty()) {
            return context;
        }
        long evaluateMillis = Timestamp.valueOf(evaluateTime).getTime();
        long windowStartMillis = resolveWindowStartMillis(evaluateTime, granularity);
        for (Map.Entry<String, List<InfluxPointDataWriter.PointValue>> entry : variableSeriesMap.entrySet()) {
            InfluxPointDataWriter.PointValue latest = findLatestInWindow(entry.getValue(), windowStartMillis, evaluateMillis);
            if (latest == null && granularity == EvaluateGranularity.MONTH) {
                latest = findLatestInWindow(entry.getValue(), Long.MIN_VALUE, evaluateMillis);
            }
            if (latest == null || latest.getPointValue() == null) {
                continue;
            }
            context.put(entry.getKey(), latest.getPointValue());
        }
        return context;
    }

    private long resolveWindowStartMillis(LocalDateTime evaluateTime, EvaluateGranularity granularity) {
        if (granularity == EvaluateGranularity.HOUR) {
            return Timestamp.valueOf(evaluateTime.withMinute(0).withSecond(0).withNano(0)).getTime();
        }
        if (granularity == EvaluateGranularity.DAY) {
            return Timestamp.valueOf(evaluateTime.withHour(0).withMinute(0).withSecond(0).withNano(0)).getTime();
        }
        return Long.MIN_VALUE;
    }

    private InfluxPointDataWriter.PointValue findLatestInWindow(List<InfluxPointDataWriter.PointValue> values,
                                                                long startMillisInclusive,
                                                                long endMillisInclusive) {
        if (values == null || values.isEmpty()) {
            return null;
        }
        InfluxPointDataWriter.PointValue latest = null;
        for (InfluxPointDataWriter.PointValue value : values) {
            if (value == null || value.getDataTime() == null) {
                continue;
            }
            long currentMillis = value.getDataTime().getTime();
            if (currentMillis > endMillisInclusive) {
                break;
            }
            if (currentMillis < startMillisInclusive) {
                continue;
            }
            latest = value;
        }
        return latest;
    }

    private enum EvaluateGranularity {
        DAY,
        HOUR,
        MONTH
    }

    private void prepareAutoPeriodDiffContextValues(String expression,
                                                    Map<String, BigDecimal> contextValues,
                                                    LocalDateTime evaluateTime,
                                                    Map<String, PeriodDiffState> periodDiffStateMap) {
        if (StringUtils.isBlank(expression) || contextValues == null || contextValues.isEmpty()) {
            return;
        }
        prepareSingleAutoDiffContext(expression, contextValues, evaluateTime, "DAY_DIFF",
                AUTO_DAY_DIFF_FUNCTION_PATTERN, "yyyy-MM-dd", periodDiffStateMap);
        prepareSingleAutoDiffContext(expression, contextValues, evaluateTime, "MONTH_DIFF",
                AUTO_MONTH_DIFF_FUNCTION_PATTERN, "yyyy-MM", periodDiffStateMap);
        prepareSingleAutoDiffContext(expression, contextValues, evaluateTime, "HOUR_DIFF",
                AUTO_HOUR_DIFF_FUNCTION_PATTERN, "yyyy-MM-dd HH", periodDiffStateMap);
    }

    private void prepareSingleAutoDiffContext(String expression,
                                              Map<String, BigDecimal> contextValues,
                                              LocalDateTime evaluateTime,
                                              String functionName,
                                              Pattern functionPattern,
                                              String periodFormat,
                                              Map<String, PeriodDiffState> periodDiffStateMap) {
        Set<String> variables = extractAutoDiffVariables(expression, functionPattern);
        if (variables.isEmpty()) {
            return;
        }
        String currentPeriod = DateUtils.parseDateToStr(periodFormat, Timestamp.valueOf(evaluateTime));
        for (String variable : variables) {
            BigDecimal currentValue = contextValues.get(variable);
            if (currentValue == null) {
                continue;
            }
            BigDecimal diffValue = calculateAutoPeriodDiffValue(functionName, variable, currentValue, currentPeriod, periodDiffStateMap);
            contextValues.put(resolveAutoDiffContextKey(functionName, variable), diffValue);
        }
    }

    private Set<String> extractAutoDiffVariables(String expression, Pattern functionPattern) {
        Set<String> variables = new HashSet<>();
        java.util.regex.Matcher matcher = functionPattern.matcher(expression);
        while (matcher.find()) {
            String variable = StringUtils.defaultString(matcher.group(1)).trim().toUpperCase(Locale.ROOT);
            if (StringUtils.isNotBlank(variable)) {
                variables.add(variable);
            }
        }
        return variables;
    }

    private BigDecimal calculateAutoPeriodDiffValue(String functionName,
                                                    String variable,
                                                    BigDecimal currentValue,
                                                    String currentPeriod,
                                                    Map<String, PeriodDiffState> periodDiffStateMap) {
        if (currentValue == null || StringUtils.isAnyBlank(functionName, variable, currentPeriod)) {
            return BigDecimal.ZERO;
        }
        String stateKey = functionName.toUpperCase(Locale.ROOT) + "_" + variable.toUpperCase(Locale.ROOT);
        PeriodDiffState state = periodDiffStateMap.computeIfAbsent(stateKey, k -> new PeriodDiffState());
        if (!StringUtils.equals(state.period, currentPeriod)) {
            state.baseline = state.lastValue == null ? currentValue : state.lastValue;
            state.period = currentPeriod;
            state.periodDiffValue = currentValue.subtract(state.baseline);
        } else if (state.baseline == null) {
            state.baseline = currentValue;
            state.periodDiffValue = currentValue.subtract(state.baseline);
        } else {
            state.periodDiffValue = currentValue.subtract(state.baseline);
        }
        state.lastValue = currentValue;
        return state.periodDiffValue == null ? BigDecimal.ZERO : state.periodDiffValue;
    }

    private static String resolveAutoDayDiffContextKey(String variable) {
        return AUTO_DAY_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(Locale.ROOT);
    }

    private static String resolveAutoMonthDiffContextKey(String variable) {
        return AUTO_MONTH_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(Locale.ROOT);
    }

    private static String resolveAutoHourDiffContextKey(String variable) {
        return AUTO_HOUR_DIFF_CONTEXT_PREFIX + StringUtils.defaultString(variable).trim().toUpperCase(Locale.ROOT);
    }

    private static String resolveAutoDiffContextKey(String functionName, String variable) {
        if ("DAY_DIFF".equalsIgnoreCase(functionName)) {
            return resolveAutoDayDiffContextKey(variable);
        }
        if ("MONTH_DIFF".equalsIgnoreCase(functionName)) {
            return resolveAutoMonthDiffContextKey(variable);
        }
        if ("HOUR_DIFF".equalsIgnoreCase(functionName)) {
            return resolveAutoHourDiffContextKey(variable);
        }
        return null;
    }

    private PointConfigLatestValueVo queryLatestValueByPointIdFromRedis(PointConfigLatestValueItemVo item) {
        PointConfigLatestValueVo vo = new PointConfigLatestValueVo();
        vo.setSiteId(item.getSiteId());
        vo.setDeviceId(item.getDeviceId());
        vo.setDataKey(item.getDataKey());

        String pointId = StringUtils.trimToNull(item.getPointId());
        vo.setPointId(pointId);
        if (StringUtils.isBlank(pointId)) {
            return vo;
        }

        String hitRedisKey = pointId;
        Object raw = redisCache.getCacheObject(pointId);
        if (raw == null) {
            log.warn("latestValues Redis未命中，siteId: {}, deviceId: {}, pointId: {}, dataKey: {}",
                    item.getSiteId(), item.getDeviceId(), pointId, item.getDataKey());
            return vo;
        }
        log.info("latestValues Redis命中，siteId: {}, deviceId: {}, pointId: {}, redisKey: {}, rawType: {}",
                item.getSiteId(), item.getDeviceId(), pointId, hitRedisKey, raw.getClass().getName());

        JSONObject root = toJsonObject(raw);
        if (root == null) {
            log.warn("latestValues Redis值无法转JSON，siteId: {}, deviceId: {}, pointId: {}, redisKey: {}, raw: {}",
                    item.getSiteId(), item.getDeviceId(), pointId, hitRedisKey, raw);
            return vo;
        }

        Object rawValue = root.get("pointValue");
        BigDecimal pointValue = toBigDecimalOrNull(rawValue);
        if (pointValue == null) {
            pointValue = readBigDecimalField(raw, "pointValue");
        }
        if (pointValue != null) {
            vo.setPointValue(pointValue);
        }

        if (StringUtils.isBlank(vo.getSiteId())) {
            vo.setSiteId(root.getString("siteId"));
            if (StringUtils.isBlank(vo.getSiteId())) {
                vo.setSiteId(readStringField(raw, "siteId"));
            }
        }
        if (StringUtils.isBlank(vo.getDeviceId())) {
            vo.setDeviceId(root.getString("deviceId"));
            if (StringUtils.isBlank(vo.getDeviceId())) {
                vo.setDeviceId(readStringField(raw, "deviceId"));
            }
        }
        if (StringUtils.isBlank(vo.getPointId())) {
            vo.setPointId(root.getString("pointId"));
            if (StringUtils.isBlank(vo.getPointId())) {
                vo.setPointId(readStringField(raw, "pointId"));
            }
        }
        Date dataTime = extractCacheDataTime(root);
        if (dataTime == null) {
            dataTime = readDateField(raw, "dataTime");
        }
        vo.setDataTime(dataTime);
        log.info("latestValues 解析完成，siteId: {}, deviceId: {}, pointId: {}, dataKey: {}, pointValue: {}, dataTime: {}",
                vo.getSiteId(), vo.getDeviceId(), vo.getPointId(), vo.getDataKey(), vo.getPointValue(), vo.getDataTime());
        return vo;
    }

    private BigDecimal toBigDecimalOrNull(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof BigDecimal) {
            return (BigDecimal) value;
        }
        try {
            return new BigDecimal(value.toString());
        } catch (Exception ex) {
            return null;
        }
    }

    private String readStringField(Object source, String fieldName) {
        Object value = readFieldValue(source, fieldName);
        return value == null ? null : String.valueOf(value);
    }

    private BigDecimal readBigDecimalField(Object source, String fieldName) {
        Object value = readFieldValue(source, fieldName);
        return toBigDecimalOrNull(value);
    }

    private Date readDateField(Object source, String fieldName) {
        Object value = readFieldValue(source, fieldName);
        if (value instanceof Date) {
            return (Date) value;
        }
        return null;
    }

    private Object readFieldValue(Object source, String fieldName) {
        if (source == null || StringUtils.isBlank(fieldName)) {
            return null;
        }
        Class<?> current = source.getClass();
        while (current != null) {
            try {
                Field field = current.getDeclaredField(fieldName);
                field.setAccessible(true);
                return field.get(source);
            } catch (NoSuchFieldException ex) {
                current = current.getSuperclass();
            } catch (Exception ex) {
                return null;
            }
        }
        return null;
    }

    private Date extractCacheDataTime(JSONObject root) {
        if (root == null) {
            return null;
        }
        Date dataTime = root.getDate("dataTime");
        if (dataTime != null) {
            return dataTime;
        }
        Long timestamp = root.getLong("timestamp");
        if (timestamp != null) {
            return DateUtils.convertUpdateTime(timestamp);
        }
        return null;
    }

    private List<PointConfigCurveValueVo> queryCurveDataFromInflux(String siteId, String pointId, Date startTime, Date endTime) {
        if (StringUtils.isBlank(pointId)) {
            return new ArrayList<>();
        }
        List<InfluxPointDataWriter.PointValue> values = influxPointDataWriter.queryCurveDataByPointKey(siteId, pointId.trim(), startTime, endTime);
        if (values == null || values.isEmpty()) {
            return new ArrayList<>();
        }
        return values.stream().map(value -> {
            PointConfigCurveValueVo vo = new PointConfigCurveValueVo();
            vo.setDataTime(value.getDataTime());
            vo.setPointValue(value.getPointValue());
            return vo;
        }).collect(Collectors.toList());
    }

    private Date[] resolveTimeRange(PointConfigCurveRequest request) {
        LocalDateTime end = LocalDateTime.now();
        String rangeType = StringUtils.defaultIfBlank(request.getRangeType(), "day").toLowerCase(Locale.ROOT);
        LocalDateTime start;
        if ("custom".equals(rangeType)) {
            if (StringUtils.isAnyBlank(request.getStartTime(), request.getEndTime())) {
                throw new ServiceException("自定义时间范围必须传入开始时间和结束时间");
            }
            start = parseDateTime(request.getStartTime());
            end = parseDateTime(request.getEndTime());
        } else if ("week".equals(rangeType)) {
            start = end.minusDays(7);
        } else if ("month".equals(rangeType)) {
            start = end.minusDays(30);
        } else {
            start = end.minusDays(1);
        }
        if (start.isAfter(end)) {
            throw new ServiceException("开始时间不能晚于结束时间");
        }
        return new Date[]{
                Timestamp.valueOf(start),
                Timestamp.valueOf(end)
        };
    }

    private LocalDateTime parseDateTime(String value) {
        try {
            return LocalDateTime.parse(value, DATETIME_FORMATTER);
        } catch (DateTimeParseException ex) {
            throw new ServiceException("时间格式错误，请使用 yyyy-MM-dd HH:mm:ss");
        }
    }

    private RuntimeException translatePointConfigPersistenceException(Exception ex, String pointId) {
        if (isPointIdDuplicateException(ex)) {
            String normalizedPointId = StringUtils.trim(pointId);
            if (StringUtils.isBlank(normalizedPointId)) {
                return new ServiceException("点位ID已存在（point_id 全局唯一），请修改后重试");
            }
            return new ServiceException("点位ID已存在：" + normalizedPointId + "（point_id 全局唯一），请修改后重试");
        }
        if (ex instanceof RuntimeException) {
            return (RuntimeException) ex;
        }
        return new ServiceException("点位配置保存失败，请稍后重试");
    }

    private boolean isPointIdDuplicateException(Throwable throwable) {
        Throwable current = throwable;
        while (current != null) {
            if (current instanceof DuplicateKeyException || current instanceof DataIntegrityViolationException) {
                String message = current.getMessage();
                if (StringUtils.isNotBlank(message) && message.toLowerCase(Locale.ROOT).contains("uk_point_id")) {
                    return true;
                }
            }
            String message = current.getMessage();
            if (StringUtils.isNotBlank(message)) {
                String lowerMessage = message.toLowerCase(Locale.ROOT);
                if (lowerMessage.contains("duplicate entry") && lowerMessage.contains("uk_point_id")) {
                    return true;
                }
            }
            current = current.getCause();
        }
        return false;
    }

    private void invalidatePointConfigCache(String siteId, String deviceId) {
        if (StringUtils.isBlank(siteId)) {
            return;
        }
        if (StringUtils.isBlank(deviceId)) {
            invalidatePointConfigCacheBySite(siteId);
            return;
        }
        Collection<String> keys = redisCache.keys(RedisKeyConstants.POINT_CONFIG_DEVICE + siteId + "_" + deviceId + "_*");
        if (keys != null && !keys.isEmpty()) {
            redisCache.deleteObject(keys);
        }
    }

    private void invalidatePointConfigCacheBySite(String siteId) {
        if (StringUtils.isBlank(siteId)) {
            return;
        }
        Collection<String> keys = redisCache.keys(RedisKeyConstants.POINT_CONFIG_DEVICE + siteId + "_*");
        if (keys != null && !keys.isEmpty()) {
            redisCache.deleteObject(keys);
        }
        Collection<String> pointKeys = redisCache.keys(RedisKeyConstants.POINT_CONFIG_POINT + siteId + "_*");
        if (pointKeys != null && !pointKeys.isEmpty()) {
            redisCache.deleteObject(pointKeys);
        }
    }

    private JSONObject toJsonObject(Object raw) {
        if (raw == null) {
            return null;
        }
        if (raw instanceof JSONObject) {
            return (JSONObject) raw;
        }
        try {
            return JSON.parseObject(JSON.toJSONString(raw));
        } catch (Exception ex) {
            return null;
        }
    }

    private JSONObject extractDataObject(JSONObject root) {
        if (root == null) {
            return null;
        }
        JSONObject dataObject = root.getJSONObject("Data");
        if (dataObject != null) {
            return dataObject;
        }
        String dataJson = root.getString("Data");
        if (StringUtils.isBlank(dataJson)) {
            return null;
        }
        try {
            return JSON.parseObject(dataJson);
        } catch (Exception ex) {
            return null;
        }
    }

    private Date extractDataTime(JSONObject root) {
        if (root == null) {
            return null;
        }
        Long timestamp = root.getLong("timestamp");
        if (timestamp != null) {
            return DateUtils.convertUpdateTime(timestamp);
        }
        return null;
    }

    private Object getValueIgnoreCase(JSONObject dataObject, String dataKey) {
        if (dataObject == null || StringUtils.isBlank(dataKey)) {
            return null;
        }
        Object directValue = dataObject.get(dataKey);
        if (directValue != null) {
            return directValue;
        }
        for (String key : dataObject.keySet()) {
            if (key != null && key.equalsIgnoreCase(dataKey)) {
                return dataObject.get(key);
            }
        }
        return null;
    }

    private EmsPointConfig getPointConfig(String siteId,
                                          String deviceId,
                                          String dataKey,
                                          Map<String, Map<String, EmsPointConfig>> configMapCache) {
        if (StringUtils.isAnyBlank(siteId, deviceId, dataKey)) {
            return null;
        }
        String cacheKey = siteId + "|" + deviceId;
        Map<String, EmsPointConfig> configByKey = configMapCache.get(cacheKey);
        if (configByKey == null) {
            EmsPointConfig query = new EmsPointConfig();
            query.setSiteId(siteId);
            query.setDeviceId(deviceId);
            List<EmsPointConfig> configList = emsPointConfigMapper.selectEmsPointConfigList(query);
            configByKey = new HashMap<>();
            if (configList != null) {
                for (EmsPointConfig config : configList) {
                    if (config != null && StringUtils.isNotBlank(config.getDataKey())) {
                        configByKey.put(config.getDataKey().toUpperCase(Locale.ROOT), config);
                    }
                }
            }
            configMapCache.put(cacheKey, configByKey);
        }
        return configByKey.get(dataKey.toUpperCase(Locale.ROOT));
    }

    private EmsPointConfig selectPointConfigByExactPointId(String siteId, String pointId) {
        if (StringUtils.isAnyBlank(siteId, pointId)) {
            return null;
        }
        String normalizedPointId = pointId.trim();
        String cacheKey = RedisKeyConstants.POINT_CONFIG_POINT + siteId + "_" + normalizedPointId;
        EmsPointConfig cached = redisCache.getCacheObject(cacheKey);
        if (cached != null) {
            return cached;
        }
        EmsPointConfig query = new EmsPointConfig();
        query.setSiteId(siteId);
        query.setPointId(normalizedPointId);
        List<EmsPointConfig> pointConfigs = emsPointConfigMapper.selectEmsPointConfigList(query);
        if (CollectionUtils.isEmpty(pointConfigs)) {
            return null;
        }
        for (EmsPointConfig pointConfig : pointConfigs) {
            if (pointConfig != null && StringUtils.equals(pointConfig.getPointId(), normalizedPointId)) {
                redisCache.setCacheObject(cacheKey, pointConfig);
                return pointConfig;
            }
        }
        return null;
    }

    private BigDecimal convertPointValue(BigDecimal sourceValue, EmsPointConfig pointConfig) {
        if (sourceValue == null || pointConfig == null) {
            return sourceValue;
        }
        BigDecimal a = pointConfig.getDataA() == null ? BigDecimal.ZERO : pointConfig.getDataA();
        BigDecimal k = pointConfig.getDataK() == null ? BigDecimal.ONE : pointConfig.getDataK();
        BigDecimal b = pointConfig.getDataB() == null ? BigDecimal.ZERO : pointConfig.getDataB();
        return a.multiply(sourceValue).multiply(sourceValue)
                .add(k.multiply(sourceValue))
                .add(b);
    }

    private static class PeriodDiffState {
        private String period;
        private BigDecimal baseline;
        private BigDecimal lastValue;
        private BigDecimal periodDiffValue;
    }

    private static class MissingVariableException extends IllegalArgumentException {
        private MissingVariableException(String variable) {
            super("缺少变量: " + variable);
        }
    }

    private enum ExprTokenType {
        NUMBER,
        STRING,
        IDENTIFIER,
        OPERATOR,
        LEFT_PAREN,
        RIGHT_PAREN,
        COMMA,
        QUESTION,
        COLON,
        EOF
    }

    private static class ExprToken {
        private final ExprTokenType type;
        private final String text;
        private final BigDecimal number;
        private final String stringValue;

        private ExprToken(ExprTokenType type, String text, BigDecimal number, String stringValue) {
            this.type = type;
            this.text = text;
            this.number = number;
            this.stringValue = stringValue;
        }

        private static ExprToken number(BigDecimal value) {
            return new ExprToken(ExprTokenType.NUMBER, null, value, null);
        }

        private static ExprToken string(String value) {
            return new ExprToken(ExprTokenType.STRING, null, null, value);
        }

        private static ExprToken identifier(String value) {
            return new ExprToken(ExprTokenType.IDENTIFIER, value, null, null);
        }

        private static ExprToken operator(String value) {
            return new ExprToken(ExprTokenType.OPERATOR, value, null, null);
        }

        private static ExprToken symbol(ExprTokenType type, String text) {
            return new ExprToken(type, text, null, null);
        }
    }

    private static class ExpressionValue {
        private final BigDecimal numberValue;
        private final String textValue;

        private ExpressionValue(BigDecimal numberValue, String textValue) {
            this.numberValue = numberValue;
            this.textValue = textValue;
        }

        private static ExpressionValue ofNumber(BigDecimal numberValue) {
            return new ExpressionValue(numberValue, null);
        }

        private static ExpressionValue ofText(String textValue) {
            return new ExpressionValue(null, textValue == null ? "" : textValue);
        }

        private boolean isNumber() {
            return numberValue != null;
        }

        private BigDecimal asNumber() {
            if (numberValue == null) {
                throw new IllegalArgumentException("表达式值不是数值类型: " + textValue);
            }
            return numberValue;
        }

        private String asText() {
            return numberValue != null ? numberValue.stripTrailingZeros().toPlainString() : textValue;
        }

        private boolean asBoolean() {
            return isNumber() ? BigDecimal.ZERO.compareTo(numberValue) != 0 : StringUtils.isNotBlank(textValue);
        }
    }

    private interface ExpressionNode {
        ExpressionValue evaluate(Map<String, BigDecimal> contextValues);
    }

    private static class NumberNode implements ExpressionNode {
        private final BigDecimal value;

        private NumberNode(BigDecimal value) {
            this.value = value;
        }

        @Override
        public ExpressionValue evaluate(Map<String, BigDecimal> contextValues) {
            return ExpressionValue.ofNumber(value);
        }
    }

    private static class StringNode implements ExpressionNode {
        private final String value;

        private StringNode(String value) {
            this.value = value;
        }

        @Override
        public ExpressionValue evaluate(Map<String, BigDecimal> contextValues) {
            return ExpressionValue.ofText(value);
        }
    }

    private static class VariableNode implements ExpressionNode {
        private final String name;

        private VariableNode(String name) {
            this.name = name;
        }

        @Override
        public ExpressionValue evaluate(Map<String, BigDecimal> contextValues) {
            BigDecimal value = contextValues.get(name);
            if (value == null) {
                throw new MissingVariableException(name);
            }
            return ExpressionValue.ofNumber(value);
        }

        private String getName() {
            return name;
        }
    }

    private static class UnaryNode implements ExpressionNode {
        private final String operator;
        private final ExpressionNode node;

        private UnaryNode(String operator, ExpressionNode node) {
            this.operator = operator;
            this.node = node;
        }

        @Override
        public ExpressionValue evaluate(Map<String, BigDecimal> contextValues) {
            ExpressionValue value = node.evaluate(contextValues);
            switch (operator) {
                case "+":
                    return value;
                case "-":
                    return ExpressionValue.ofNumber(value.asNumber().negate());
                case "!":
                    return value.asBoolean() ? ExpressionValue.ofNumber(BigDecimal.ZERO) : ExpressionValue.ofNumber(BigDecimal.ONE);
                default:
                    throw new IllegalArgumentException("不支持的一元操作符: " + operator);
            }
        }
    }

    private static class BinaryNode implements ExpressionNode {
        private final String operator;
        private final ExpressionNode left;
        private final ExpressionNode right;

        private BinaryNode(String operator, ExpressionNode left, ExpressionNode right) {
            this.operator = operator;
            this.left = left;
            this.right = right;
        }

        @Override
        public ExpressionValue evaluate(Map<String, BigDecimal> contextValues) {
            if ("&&".equals(operator)) {
                ExpressionValue leftValue = left.evaluate(contextValues);
                if (!leftValue.asBoolean()) {
                    return ExpressionValue.ofNumber(BigDecimal.ZERO);
                }
                return right.evaluate(contextValues).asBoolean()
                        ? ExpressionValue.ofNumber(BigDecimal.ONE)
                        : ExpressionValue.ofNumber(BigDecimal.ZERO);
            }
            if ("||".equals(operator)) {
                ExpressionValue leftValue = left.evaluate(contextValues);
                if (leftValue.asBoolean()) {
                    return ExpressionValue.ofNumber(BigDecimal.ONE);
                }
                return right.evaluate(contextValues).asBoolean()
                        ? ExpressionValue.ofNumber(BigDecimal.ONE)
                        : ExpressionValue.ofNumber(BigDecimal.ZERO);
            }

            ExpressionValue leftValue = left.evaluate(contextValues);
            ExpressionValue rightValue = right.evaluate(contextValues);
            switch (operator) {
                case "+":
                    if (!leftValue.isNumber() || !rightValue.isNumber()) {
                        return ExpressionValue.ofText(leftValue.asText() + rightValue.asText());
                    }
                    return ExpressionValue.ofNumber(leftValue.asNumber().add(rightValue.asNumber()));
                case "-":
                    return ExpressionValue.ofNumber(leftValue.asNumber().subtract(rightValue.asNumber()));
                case "*":
                    return ExpressionValue.ofNumber(leftValue.asNumber().multiply(rightValue.asNumber()));
                case "/":
                    if (BigDecimal.ZERO.compareTo(rightValue.asNumber()) == 0) {
                        throw new IllegalArgumentException("除数不能为0");
                    }
                    return ExpressionValue.ofNumber(leftValue.asNumber().divide(rightValue.asNumber(), 10, RoundingMode.HALF_UP));
                case ">":
                    return leftValue.asNumber().compareTo(rightValue.asNumber()) > 0
                            ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO);
                case ">=":
                    return leftValue.asNumber().compareTo(rightValue.asNumber()) >= 0
                            ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO);
                case "<":
                    return leftValue.asNumber().compareTo(rightValue.asNumber()) < 0
                            ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO);
                case "<=":
                    return leftValue.asNumber().compareTo(rightValue.asNumber()) <= 0
                            ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO);
                case "==":
                    if (leftValue.isNumber() && rightValue.isNumber()) {
                        return leftValue.asNumber().compareTo(rightValue.asNumber()) == 0
                                ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO);
                    }
                    return Objects.equals(leftValue.asText(), rightValue.asText())
                            ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO);
                case "!=":
                    if (leftValue.isNumber() && rightValue.isNumber()) {
                        return leftValue.asNumber().compareTo(rightValue.asNumber()) != 0
                                ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO);
                    }
                    return !Objects.equals(leftValue.asText(), rightValue.asText())
                            ? ExpressionValue.ofNumber(BigDecimal.ONE) : ExpressionValue.ofNumber(BigDecimal.ZERO);
                default:
                    throw new IllegalArgumentException("不支持的操作符: " + operator);
            }
        }
    }

    private static class TernaryNode implements ExpressionNode {
        private final ExpressionNode condition;
        private final ExpressionNode trueNode;
        private final ExpressionNode falseNode;

        private TernaryNode(ExpressionNode condition, ExpressionNode trueNode, ExpressionNode falseNode) {
            this.condition = condition;
            this.trueNode = trueNode;
            this.falseNode = falseNode;
        }

        @Override
        public ExpressionValue evaluate(Map<String, BigDecimal> contextValues) {
            return condition.evaluate(contextValues).asBoolean()
                    ? trueNode.evaluate(contextValues)
                    : falseNode.evaluate(contextValues);
        }
    }

    private static class FunctionNode implements ExpressionNode {
        private final String functionName;
        private final List<ExpressionNode> arguments;

        private FunctionNode(String functionName, List<ExpressionNode> arguments) {
            this.functionName = StringUtils.defaultString(functionName).trim().toUpperCase(Locale.ROOT);
            this.arguments = arguments == null ? Collections.emptyList() : arguments;
        }

        @Override
        public ExpressionValue evaluate(Map<String, BigDecimal> contextValues) {
            if (("DAY_DIFF".equals(functionName)
                    || "MONTH_DIFF".equals(functionName)
                    || "HOUR_DIFF".equals(functionName)) && arguments.size() == 1) {
                ExpressionNode argument = arguments.get(0);
                if (!(argument instanceof VariableNode)) {
                    throw new IllegalArgumentException(functionName + "单参数模式仅支持变量参数");
                }
                String variable = ((VariableNode) argument).getName();
                BigDecimal autoDiffValue = contextValues.get(resolveAutoDiffContextKey(functionName, variable));
                if (autoDiffValue == null) {
                    throw new MissingVariableException(variable);
                }
                return ExpressionValue.ofNumber(autoDiffValue);
            }
            if ("DAY_DIFF".equals(functionName)
                    || "MONTH_DIFF".equals(functionName)
                    || "HOUR_DIFF".equals(functionName)) {
                throw new IllegalArgumentException(functionName + "函数参数数量错误，需1个参数");
            }
            throw new IllegalArgumentException("不支持的函数: " + functionName);
        }
    }

    private static class CompiledExpression {
        private final ExpressionNode root;

        private CompiledExpression(String expression) {
            List<ExprToken> tokens = tokenizeExpression(expression);
            ExpressionParser parser = new ExpressionParser(tokens);
            this.root = parser.parseExpression();
            if (!parser.isEnd()) {
                ExprToken token = parser.peek();
                throw new IllegalArgumentException("表达式尾部有多余内容: " + token.text);
            }
        }

        private ExpressionValue evaluate(Map<String, BigDecimal> contextValues) {
            return root.evaluate(contextValues);
        }
    }

    private static class ExpressionParser {
        private final List<ExprToken> tokens;
        private int index;

        private ExpressionParser(List<ExprToken> tokens) {
            this.tokens = tokens;
            this.index = 0;
        }

        private ExpressionNode parseExpression() {
            return parseTernary();
        }

        private ExpressionNode parseTernary() {
            ExpressionNode condition = parseOr();
            if (match(ExprTokenType.QUESTION)) {
                ExpressionNode trueNode = parseTernary();
                expect(ExprTokenType.COLON, "三元表达式缺少 ':'");
                ExpressionNode falseNode = parseTernary();
                return new TernaryNode(condition, trueNode, falseNode);
            }
            return condition;
        }

        private ExpressionNode parseOr() {
            ExpressionNode left = parseAnd();
            while (matchOperator("||")) {
                left = new BinaryNode("||", left, parseAnd());
            }
            return left;
        }

        private ExpressionNode parseAnd() {
            ExpressionNode left = parseEquality();
            while (matchOperator("&&")) {
                left = new BinaryNode("&&", left, parseEquality());
            }
            return left;
        }

        private ExpressionNode parseEquality() {
            ExpressionNode left = parseComparison();
            while (true) {
                if (matchOperator("==")) {
                    left = new BinaryNode("==", left, parseComparison());
                    continue;
                }
                if (matchOperator("!=")) {
                    left = new BinaryNode("!=", left, parseComparison());
                    continue;
                }
                break;
            }
            return left;
        }

        private ExpressionNode parseComparison() {
            ExpressionNode left = parseAddSub();
            while (true) {
                if (matchOperator(">=")) {
                    left = new BinaryNode(">=", left, parseAddSub());
                    continue;
                }
                if (matchOperator("<=")) {
                    left = new BinaryNode("<=", left, parseAddSub());
                    continue;
                }
                if (matchOperator(">")) {
                    left = new BinaryNode(">", left, parseAddSub());
                    continue;
                }
                if (matchOperator("<")) {
                    left = new BinaryNode("<", left, parseAddSub());
                    continue;
                }
                break;
            }
            return left;
        }

        private ExpressionNode parseAddSub() {
            ExpressionNode left = parseMulDiv();
            while (true) {
                if (matchOperator("+")) {
                    left = new BinaryNode("+", left, parseMulDiv());
                    continue;
                }
                if (matchOperator("-")) {
                    left = new BinaryNode("-", left, parseMulDiv());
                    continue;
                }
                break;
            }
            return left;
        }

        private ExpressionNode parseMulDiv() {
            ExpressionNode left = parseUnary();
            while (true) {
                if (matchOperator("*")) {
                    left = new BinaryNode("*", left, parseUnary());
                    continue;
                }
                if (matchOperator("/")) {
                    left = new BinaryNode("/", left, parseUnary());
                    continue;
                }
                break;
            }
            return left;
        }

        private ExpressionNode parseUnary() {
            if (matchOperator("+")) {
                return new UnaryNode("+", parseUnary());
            }
            if (matchOperator("-")) {
                return new UnaryNode("-", parseUnary());
            }
            if (matchOperator("!")) {
                return new UnaryNode("!", parseUnary());
            }
            return parsePrimary();
        }

        private ExpressionNode parsePrimary() {
            ExprToken token = peek();
            if (match(ExprTokenType.NUMBER)) {
                return new NumberNode(token.number);
            }
            if (match(ExprTokenType.STRING)) {
                return new StringNode(token.stringValue);
            }
            if (match(ExprTokenType.IDENTIFIER)) {
                String identifier = token.text;
                if (match(ExprTokenType.LEFT_PAREN)) {
                    return parseFunction(identifier);
                }
                return new VariableNode(identifier.toUpperCase(Locale.ROOT));
            }
            if (match(ExprTokenType.LEFT_PAREN)) {
                ExpressionNode nested = parseExpression();
                expect(ExprTokenType.RIGHT_PAREN, "括号不匹配，缺少右括号");
                return nested;
            }
            throw new IllegalArgumentException("表达式语法错误，当前位置: " + token.text);
        }

        private ExpressionNode parseFunction(String identifier) {
            if ("IF".equalsIgnoreCase(identifier)) {
                ExpressionNode condition = parseExpression();
                expect(ExprTokenType.COMMA, "IF函数参数格式错误，缺少第1个逗号");
                ExpressionNode trueNode = parseExpression();
                expect(ExprTokenType.COMMA, "IF函数参数格式错误，缺少第2个逗号");
                ExpressionNode falseNode = parseExpression();
                expect(ExprTokenType.RIGHT_PAREN, "IF函数缺少右括号");
                return new TernaryNode(condition, trueNode, falseNode);
            }
            if ("DAY_DIFF".equalsIgnoreCase(identifier)
                    || "MONTH_DIFF".equalsIgnoreCase(identifier)
                    || "HOUR_DIFF".equalsIgnoreCase(identifier)) {
                List<ExpressionNode> arguments = parseFunctionArguments(identifier);
                if (arguments.size() != 1) {
                    throw new IllegalArgumentException(identifier.toUpperCase(Locale.ROOT) + "函数参数数量错误，需1个参数");
                }
                return new FunctionNode(identifier, arguments);
            }
            throw new IllegalArgumentException("不支持的函数: " + identifier);
        }

        private List<ExpressionNode> parseFunctionArguments(String functionName) {
            List<ExpressionNode> arguments = new ArrayList<>();
            if (match(ExprTokenType.RIGHT_PAREN)) {
                return arguments;
            }
            while (true) {
                arguments.add(parseExpression());
                if (match(ExprTokenType.COMMA)) {
                    continue;
                }
                expect(ExprTokenType.RIGHT_PAREN, functionName + "函数缺少右括号");
                return arguments;
            }
        }

        private ExprToken peek() {
            if (index >= tokens.size()) {
                return ExprToken.symbol(ExprTokenType.EOF, "<eof>");
            }
            return tokens.get(index);
        }

        private boolean isEnd() {
            return peek().type == ExprTokenType.EOF;
        }

        private boolean match(ExprTokenType type) {
            if (peek().type != type) {
                return false;
            }
            index++;
            return true;
        }

        private boolean matchOperator(String operator) {
            ExprToken token = peek();
            if (token.type != ExprTokenType.OPERATOR || !operator.equals(token.text)) {
                return false;
            }
            index++;
            return true;
        }

        private void expect(ExprTokenType type, String message) {
            if (!match(type)) {
                throw new IllegalArgumentException(message);
            }
        }
    }

    private static List<ExprToken> tokenizeExpression(String expression) {
        if (StringUtils.isBlank(expression)) {
            throw new IllegalArgumentException("计算表达式为空");
        }
        List<ExprToken> tokens = new ArrayList<>();
        int index = 0;
        while (index < expression.length()) {
            char ch = expression.charAt(index);
            if (Character.isWhitespace(ch)) {
                index++;
                continue;
            }
            if (Character.isDigit(ch) || ch == '.') {
                int start = index;
                boolean hasDot = ch == '.';
                index++;
                while (index < expression.length()) {
                    char next = expression.charAt(index);
                    if (Character.isDigit(next)) {
                        index++;
                        continue;
                    }
                    if (next == '.' && !hasDot) {
                        hasDot = true;
                        index++;
                        continue;
                    }
                    break;
                }
                String text = expression.substring(start, index);
                try {
                    tokens.add(ExprToken.number(new BigDecimal(text)));
                } catch (NumberFormatException ex) {
                    throw new IllegalArgumentException("数值格式错误: " + text);
                }
                continue;
            }
            if (Character.isLetter(ch) || ch == '_') {
                int start = index;
                index++;
                while (index < expression.length()) {
                    char next = expression.charAt(index);
                    if (Character.isLetterOrDigit(next) || next == '_') {
                        index++;
                        continue;
                    }
                    break;
                }
                String text = expression.substring(start, index).trim().toUpperCase(Locale.ROOT);
                tokens.add(ExprToken.identifier(text));
                continue;
            }
            if (ch == '\'' || ch == '"') {
                char quote = ch;
                index++;
                StringBuilder sb = new StringBuilder();
                boolean escaped = false;
                while (index < expression.length()) {
                    char current = expression.charAt(index++);
                    if (escaped) {
                        switch (current) {
                            case 'n':
                                sb.append('\n');
                                break;
                            case 't':
                                sb.append('\t');
                                break;
                            case 'r':
                                sb.append('\r');
                                break;
                            case '\\':
                                sb.append('\\');
                                break;
                            case '\'':
                                sb.append('\'');
                                break;
                            case '"':
                                sb.append('"');
                                break;
                            default:
                                sb.append(current);
                                break;
                        }
                        escaped = false;
                        continue;
                    }
                    if (current == '\\') {
                        escaped = true;
                        continue;
                    }
                    if (current == quote) {
                        break;
                    }
                    sb.append(current);
                }
                if (escaped || index > expression.length() || expression.charAt(index - 1) != quote) {
                    throw new IllegalArgumentException("字符串字面量未闭合");
                }
                tokens.add(ExprToken.string(sb.toString()));
                continue;
            }
            if (ch == '(') {
                tokens.add(ExprToken.symbol(ExprTokenType.LEFT_PAREN, "("));
                index++;
                continue;
            }
            if (ch == ')') {
                tokens.add(ExprToken.symbol(ExprTokenType.RIGHT_PAREN, ")"));
                index++;
                continue;
            }
            if (ch == ',') {
                tokens.add(ExprToken.symbol(ExprTokenType.COMMA, ","));
                index++;
                continue;
            }
            if (ch == '?') {
                tokens.add(ExprToken.symbol(ExprTokenType.QUESTION, "?"));
                index++;
                continue;
            }
            if (ch == ':') {
                tokens.add(ExprToken.symbol(ExprTokenType.COLON, ":"));
                index++;
                continue;
            }
            if (index + 1 < expression.length()) {
                String twoChars = expression.substring(index, index + 2);
                if ("&&".equals(twoChars)
                        || "||".equals(twoChars)
                        || ">=".equals(twoChars)
                        || "<=".equals(twoChars)
                        || "==".equals(twoChars)
                        || "!=".equals(twoChars)) {
                    tokens.add(ExprToken.operator(twoChars));
                    index += 2;
                    continue;
                }
            }
            if (ch == '+' || ch == '-' || ch == '*' || ch == '/'
                    || ch == '>' || ch == '<' || ch == '!') {
                tokens.add(ExprToken.operator(String.valueOf(ch)));
                index++;
                continue;
            }
            throw new IllegalArgumentException("表达式包含非法字符: " + ch);
        }
        tokens.add(ExprToken.symbol(ExprTokenType.EOF, "<eof>"));
        return tokens;
    }

    private List<EmsPointConfig> parseCsv(MultipartFile file, String siteId) {
        List<EmsPointConfig> result = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
            String headerLine = reader.readLine();
            if (StringUtils.isBlank(headerLine)) {
                throw new ServiceException("CSV表头不能为空");
            }
            List<String> headerList = parseCsvLine(removeUtf8Bom(headerLine));
            Map<String, Integer> headerIndex = buildHeaderIndex(headerList);
            assertRequiredHeader(headerIndex, "point_id", "point_id");
            assertRequiredHeader(headerIndex, "data_key", "data_key");
            assertRequiredHeader(headerIndex, "point_desc", "point_desc");

            String line;
            int lineNo = 1;
            while ((line = reader.readLine()) != null) {
                lineNo++;
                if (StringUtils.isBlank(line)) {
                    continue;
                }
                List<String> valueList = parseCsvLine(line);
                EmsPointConfig pointConfig = new EmsPointConfig();
                pointConfig.setSiteId(siteId);
                pointConfig.setPointId(getRequiredString(valueList, headerIndex, "point_id", lineNo));
                pointConfig.setDeviceCategory(getString(valueList, headerIndex, "device_category"));
                pointConfig.setDeviceId(getString(valueList, headerIndex, "device_id"));
                pointConfig.setDataKey(getRequiredString(valueList, headerIndex, "data_key", lineNo));
                pointConfig.setPointDesc(getRequiredString(valueList, headerIndex, "point_desc", lineNo));
                String registerAddress = getString(valueList, headerIndex, "register_address");
                pointConfig.setRegisterAddress(StringUtils.isBlank(registerAddress) ? "" : registerAddress);
                pointConfig.setPointName(getString(valueList, headerIndex, "point_name"));
                pointConfig.setDataUnit(getString(valueList, headerIndex, "data_unit"));
                pointConfig.setDataA(getDecimal(valueList, headerIndex, "data_a", lineNo));
                pointConfig.setDataK(getDecimal(valueList, headerIndex, "data_k", lineNo));
                pointConfig.setDataB(getDecimal(valueList, headerIndex, "data_b", lineNo));
                pointConfig.setDataBit(getInteger(valueList, headerIndex, "data_bit", lineNo));
                pointConfig.setIsAlarm(getInteger(valueList, headerIndex, "is_alarm", lineNo));
                pointConfig.setPointType(getString(valueList, headerIndex, "point_type"));
                pointConfig.setCalcExpression(getString(valueList, headerIndex, "calc_expression"));
                pointConfig.setCollectEnabled(getInteger(valueList, headerIndex, "collect_enabled", lineNo));
                pointConfig.setCollectSource(getString(valueList, headerIndex, "collect_source"));
                pointConfig.setModbusRegisterType(getString(valueList, headerIndex, "modbus_register_type"));
                pointConfig.setModbusDataType(getString(valueList, headerIndex, "modbus_data_type"));
                pointConfig.setModbusReadOrder(getInteger(valueList, headerIndex, "modbus_read_order", lineNo));
                pointConfig.setModbusGroup(getString(valueList, headerIndex, "modbus_group"));
                pointConfig.setDecimalScale(getInteger(valueList, headerIndex, "decimal_scale", lineNo));
                pointConfig.setRemark(getString(valueList, headerIndex, "remark"));
                normalizeAndValidatePointConfig(pointConfig);
                applyCollectDefaultsForInsert(pointConfig);
                result.add(pointConfig);
            }
        } catch (IOException e) {
            throw new ServiceException("CSV读取失败: " + e.getMessage());
        }
        return result;
    }

    private void assertRequiredHeader(Map<String, Integer> headerIndex, String headerKey, String headerName) {
        if (!headerIndex.containsKey(headerKey)) {
            throw new ServiceException("CSV缺少必填列: " + headerName);
        }
    }

    private Map<String, Integer> buildHeaderIndex(List<String> headerList) {
        Map<String, Integer> headerIndex = new HashMap<>();
        for (int i = 0; i < headerList.size(); i++) {
            String normalizedHeader = toCanonicalHeaderKey(headerList.get(i));
            if (StringUtils.isBlank(normalizedHeader)) {
                continue;
            }
            headerIndex.putIfAbsent(normalizedHeader, i);
        }
        return headerIndex;
    }

    private String getRequiredString(List<String> valueList, Map<String, Integer> headerIndex, String key, int lineNo) {
        String value = getString(valueList, headerIndex, key);
        if (StringUtils.isBlank(value)) {
            throw new ServiceException(String.format("CSV第%d行字段[%s]不能为空", lineNo, key));
        }
        return value;
    }

    private String getString(List<String> valueList, Map<String, Integer> headerIndex, String key) {
        Integer index = headerIndex.get(key);
        if (index == null || index < 0 || index >= valueList.size()) {
            return null;
        }
        String value = valueList.get(index);
        return StringUtils.isBlank(value) ? null : value.trim();
    }

    private Integer getInteger(List<String> valueList, Map<String, Integer> headerIndex, String key, int lineNo) {
        String value = getString(valueList, headerIndex, key);
        if (StringUtils.isBlank(value)) {
            return null;
        }
        if (!INTEGER_PATTERN.matcher(value).matches()) {
            throw new ServiceException(String.format("CSV第%d行字段[%s]必须是整数", lineNo, key));
        }
        return Integer.parseInt(value);
    }

    private BigDecimal getDecimal(List<String> valueList, Map<String, Integer> headerIndex, String key, int lineNo) {
        String value = getString(valueList, headerIndex, key);
        if (StringUtils.isBlank(value)) {
            return null;
        }
        if (!DECIMAL_PATTERN.matcher(value).matches()) {
            throw new ServiceException(String.format("CSV第%d行字段[%s]必须是数字", lineNo, key));
        }
        return new BigDecimal(value);
    }

    private String removeUtf8Bom(String content) {
        if (StringUtils.isBlank(content)) {
            return content;
        }
        if (content.charAt(0) == '\ufeff') {
            return content.substring(1);
        }
        return content;
    }

    private String normalizeHeader(String header) {
        if (header == null) {
            return "";
        }
        return header.trim().toLowerCase().replace("-", "_").replace(" ", "");
    }

    private String toCanonicalHeaderKey(String header) {
        String normalized = normalizeHeader(header);
        if (StringUtils.isBlank(normalized)) {
            return "";
        }
        String compact = normalized.replace("_", "");
        switch (compact) {
            case "siteid":
            case "站点id":
                return "site_id";
            case "pointid":
            case "点位id":
                return "point_id";
            case "devicecategory":
            case "设备类型":
                return "device_category";
            case "deviceid":
            case "设备id":
                return "device_id";
            case "pointname":
            case "点位名称":
                return "point_name";
            case "datakey":
            case "数据键":
                return "data_key";
            case "pointdesc":
            case "点位描述":
                return "point_desc";
            case "registeraddress":
            case "寄存器地址":
                return "register_address";
            case "dataunit":
            case "datounit":
            case "单位":
                return "data_unit";
            case "dataa":
            case "datoa":
            case "a系数":
                return "data_a";
            case "datak":
            case "datok":
            case "k系数":
                return "data_k";
            case "datab":
            case "datob":
            case "b系数":
                return "data_b";
            case "databit":
            case "datobit":
            case "位偏移":
                return "data_bit";
            case "isalarm":
            case "报警点位":
                return "is_alarm";
            case "pointtype":
            case "点位类型":
                return "point_type";
            case "calcexpression":
            case "计算表达式":
                return "calc_expression";
            case "collectenabled":
            case "是否启用采集":
                return "collect_enabled";
            case "collectsource":
            case "采集来源":
                return "collect_source";
            case "modbusregistertype":
            case "modbus寄存器类型":
                return "modbus_register_type";
            case "modbusdatatype":
            case "modbus数据类型":
                return "modbus_data_type";
            case "modbusreadorder":
            case "modbus读取顺序":
                return "modbus_read_order";
            case "modbusgroup":
            case "modbus分组":
                return "modbus_group";
            case "decimalscale":
            case "小数位数":
                return "decimal_scale";
            case "remark":
            case "备注":
                return "remark";
            default:
                return normalized;
        }
    }

    private List<String> parseCsvLine(String line) {
        List<String> result = new ArrayList<>();
        if (line == null) {
            return result;
        }
        StringBuilder current = new StringBuilder();
        boolean inQuotes = false;
        for (int i = 0; i < line.length(); i++) {
            char c = line.charAt(i);
            if (c == '"') {
                if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') {
                    current.append('"');
                    i++;
                } else {
                    inQuotes = !inQuotes;
                }
                continue;
            }
            if (c == ',' && !inQuotes) {
                result.add(current.toString());
                current.setLength(0);
                continue;
            }
            current.append(c);
        }
        result.add(current.toString());
        return result;
    }

    private void normalizeAndValidatePointConfig(EmsPointConfig pointConfig) {
        if (pointConfig == null) {
            return;
        }
        pointConfig.setPointId(StringUtils.trimToNull(pointConfig.getPointId()));
        if (StringUtils.isBlank(pointConfig.getPointId())) {
            throw new ServiceException("点位ID不能为空");
        }
        pointConfig.setDeviceCategory(StringUtils.trimToNull(pointConfig.getDeviceCategory()));
        pointConfig.setDeviceId(StringUtils.trimToNull(pointConfig.getDeviceId()));
        pointConfig.setPointName(StringUtils.trimToNull(pointConfig.getPointName()));
        pointConfig.setDataKey(StringUtils.trimToNull(pointConfig.getDataKey()));
        pointConfig.setPointDesc(StringUtils.trimToNull(pointConfig.getPointDesc()));
        pointConfig.setPointType(normalizePointType(pointConfig.getPointType()));
        pointConfig.setCalcExpression(StringUtils.trimToNull(pointConfig.getCalcExpression()));
        if ("calc".equals(pointConfig.getPointType())) {
            if (StringUtils.isBlank(pointConfig.getCalcExpression())) {
                throw new ServiceException("计算点必须填写计算表达式");
            }
            if (!CALC_EXPRESSION_PATTERN.matcher(pointConfig.getCalcExpression()).matches()) {
                throw new ServiceException("计算表达式仅支持数字、字母、下划线、空格、运算符和函数语法");
            }
        } else {
            pointConfig.setCalcExpression(null);
        }
    }

    private void applyCollectDefaultsForInsert(EmsPointConfig pointConfig) {
        if (pointConfig == null) {
            return;
        }
        if (pointConfig.getCollectEnabled() == null) {
            pointConfig.setCollectEnabled(0);
        }
        if (StringUtils.isBlank(pointConfig.getCollectSource())) {
            pointConfig.setCollectSource("MQTT");
        }
        if (pointConfig.getModbusReadOrder() == null) {
            pointConfig.setModbusReadOrder(0);
        }
    }

    private String normalizePointType(String pointType) {
        String normalized = StringUtils.trimToEmpty(pointType).toLowerCase(Locale.ROOT);
        if ("calc".equals(normalized) || "calculate".equals(normalized) || "计算点".equals(normalized)) {
            return "calc";
        }
        return "data";
    }

}
