package com.ruoyi.web.modules.screen.support;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.ruoyi.web.modules.diagnosis.support.AiConsultConstants;
import com.ruoyi.web.modules.industryservice.domain.BizLivestockProductOutput;
import com.ruoyi.web.modules.industryservice.domain.BizThreeAssetsProblemReport;
import com.ruoyi.web.modules.industryservice.domain.BizVaccinationData;
import com.ruoyi.web.modules.industryservice.support.BreedingIndustryRules;
import com.ruoyi.web.modules.screen.domain.dto.LivestockResourceBandCountDto;
import com.ruoyi.web.modules.screen.domain.dto.LivestockResourceCategoryCountDto;
import com.ruoyi.web.modules.screen.domain.dto.LivestockResourceDateCountDto;
import com.ruoyi.web.modules.screen.domain.dto.LivestockResourceProblemLevelCountDto;
import com.ruoyi.web.modules.screen.domain.dto.LivestockResourceProductOutputMonthDto;
import com.ruoyi.web.modules.screen.domain.dto.LivestockResourceProductOutputSumDto;
import com.ruoyi.web.modules.screen.domain.dto.LivestockResourceTechPersonnelDto;
import com.ruoyi.web.modules.screen.domain.dto.LivestockResourceThreeAssetsSumDto;
import com.ruoyi.web.modules.screen.domain.dto.LivestockResourceUserStructureDto;
import com.ruoyi.web.modules.screen.domain.dto.LivestockResourceVaccinationSumDto;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceActivityTrendVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceAgriUnitStructureVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceCategoryItemVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceCategoryShareVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceDailyPointVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceHourlyHeatVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceHourlyPointVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceKeywordVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceOverviewVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceProblemLevelItemVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceProblemReportShareVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceProductOutputListItemVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceProductOutputMonthPointVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceProductOutputTrendVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceReplyStatusVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceRuralThreeAssetsVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceSessionBandVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceShareItemVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceStatusItemVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceTechEnvoyStructureVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceThreeAssetsProblemListItemVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceUserStructureVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceVaccinationListItemVo;
import com.ruoyi.web.modules.screen.domain.vo.LivestockResourceVaccinationOverviewVo;

/**
 * 大屏畜牧资源口径组装与纯计算（无 DB / HTTP 依赖，便于单测）。
 * <p>
 * 看板：总览、三资、问题上报占比、产量趋势、疫苗、特派员/事业单位结构等。<br>
 * 明细：三资问题上报 / 产量 / 疫苗列表行组装；等级名称、免疫率派生等。<br>
 * 另保留 AI 问诊相关组装方法（活跃度、分词关键词等），看板主接口已不再调用。
 * </p>
 *
 * @see LivestockResourceScreenRules
 */
public final class LivestockResourceScreenSupport
{
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");

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

    private static final DateTimeFormatter HOUR_FMT = DateTimeFormatter.ofPattern("HH:mm");

    private static final DateTimeFormatter HOUR_BUCKET_KEY_FMT = DateTimeFormatter.ofPattern("yyyyMMddHH");

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

    private static final int RATIO_SCALE = 1;

    private static final Pattern CN_TOKEN = Pattern.compile("[\\u4e00-\\u9fa5]{2,}");

    private static final Pattern EN_TOKEN = Pattern.compile("[A-Za-z]{2,}");

    private static final Set<String> STOP_WORDS = new HashSet<>(Arrays.asList(
            "的", "了", "是", "在", "我", "有", "和", "就", "不", "人", "都", "一", "一个", "上", "也", "很",
            "到", "说", "要", "去", "你", "会", "着", "没有", "看", "好", "自己", "这", "他", "她", "它",
            "吗", "呢", "啊", "吧", "那", "什么", "怎么", "为什么", "可以", "已经", "还是", "如果"));

    private static final String[][] CATEGORY_ORDER = {
            {"1", "养殖技巧"},
            {"2", "疾病诊疗"},
            {"3", "用药指导"},
            {"4", "设备操作"},
            {LivestockResourceScreenRules.CATEGORY_UNCAT, "未分类"}
    };

    private static final String[][] BAND_ORDER = {
            {LivestockResourceScreenRules.BAND_LIGHT, "轻度"},
            {LivestockResourceScreenRules.BAND_NORMAL, "一般"},
            {LivestockResourceScreenRules.BAND_ACTIVE, "活跃"},
            {LivestockResourceScreenRules.BAND_DEEP, "深度"}
    };

    private LivestockResourceScreenSupport()
    {
    }

    public static String formatStatDateTime(LocalDateTime dateTime)
    {
        return dateTime.format(DATE_TIME_FMT);
    }

    /** 是否当前自然年；决定是否在概览中展示「今日」四项指标。 */
    public static boolean isCurrentStatYear(int statYear)
    {
        return statYear == LocalDate.now(ZoneId.of(BreedingIndustryRules.ZONE_SHANGHAI)).getYear();
    }

    /**
     * 构建核心总览 8 项：三资、肉/奶产量、免疫率、兽医、科技特派员、农业事业单位。
     */
    public static LivestockResourceOverviewVo buildOverview(
            LivestockResourceThreeAssetsSumDto assets,
            LivestockResourceProductOutputSumDto productOutput,
            LivestockResourceVaccinationSumDto vaccination,
            Long vetCount,
            LivestockResourceTechPersonnelDto techPersonnel)
    {
        LivestockResourceOverviewVo vo = new LivestockResourceOverviewVo();
        vo.setOperatingFixedAssets(scaleMoney(assets != null ? assets.getOperatingFixedAssets() : null));
        vo.setNonOperatingFixedAssets(scaleMoney(assets != null ? assets.getNonOperatingFixedAssets() : null));
        vo.setMeatOutput(scaleOutput(productOutput != null ? productOutput.getMeatOutput() : null));
        vo.setMilkOutput(scaleOutput(productOutput != null ? productOutput.getMilkOutput() : null));
        vo.setImmunityRate(calcImmunityRate(vaccination));
        vo.setVetCount(safeLong(vetCount));
        vo.setTechEnvoyCount(techPersonnel != null ? safeLong(techPersonnel.getEnvoyTotalCount()) : 0L);
        vo.setAgriUnitCount(techPersonnel != null ? safeLong(techPersonnel.getAgriTotalCount()) : 0L);
        return vo;
    }

    /** 农村三资数据区块 */
    public static LivestockResourceRuralThreeAssetsVo buildRuralThreeAssets(LivestockResourceThreeAssetsSumDto assets)
    {
        LivestockResourceRuralThreeAssetsVo vo = new LivestockResourceRuralThreeAssetsVo();
        vo.setOperatingFixedAssets(scaleMoney(assets != null ? assets.getOperatingFixedAssets() : null));
        vo.setNonOperatingFixedAssets(scaleMoney(assets != null ? assets.getNonOperatingFixedAssets() : null));
        if (assets != null && assets.getFixedAssets() != null)
        {
            vo.setFixedAssets(scaleMoney(assets.getFixedAssets()));
        }
        else
        {
            BigDecimal operating = vo.getOperatingFixedAssets();
            BigDecimal nonOperating = vo.getNonOperatingFixedAssets();
            vo.setFixedAssets(operating.add(nonOperating).setScale(2, RoundingMode.HALF_UP));
        }
        return vo;
    }

    /** 三资问题上报：按等级占比与数量（次） */
    public static LivestockResourceProblemReportShareVo buildProblemReportShare(
            List<LivestockResourceProblemLevelCountDto> rows)
    {
        Map<String, Long> countMap = new HashMap<>();
        long total = 0L;
        if (rows != null)
        {
            for (LivestockResourceProblemLevelCountDto row : rows)
            {
                if (row == null || row.getProblemLevel() == null)
                {
                    continue;
                }
                long c = safeLong(row.getCount());
                countMap.put(row.getProblemLevel(), countMap.getOrDefault(row.getProblemLevel(), 0L) + c);
                total += c;
            }
        }
        String[][] order = {
                {LivestockResourceScreenRules.PROBLEM_LEVEL_GENERAL, "一般"},
                {LivestockResourceScreenRules.PROBLEM_LEVEL_KEY, "重点"},
                {LivestockResourceScreenRules.PROBLEM_LEVEL_MAJOR, "重大"}
        };
        List<LivestockResourceProblemLevelItemVo> items = new ArrayList<>();
        for (String[] pair : order)
        {
            long count = countMap.getOrDefault(pair[0], 0L);
            LivestockResourceProblemLevelItemVo item = new LivestockResourceProblemLevelItemVo();
            item.setLevelCode(pair[0]);
            item.setLevelName(pair[1]);
            item.setCount(count);
            item.setRatio(ratioPercent(count, total));
            items.add(item);
        }
        LivestockResourceProblemReportShareVo vo = new LivestockResourceProblemReportShareVo();
        vo.setTotalCount(total);
        vo.setItems(items);
        return vo;
    }

    /**
     * 三资具体问题上报 → 明细分页 VO；附带等级中文名。
     *
     * @param rows {@code biz_three_assets_problem_report} 列表（已按 report_time 倒序）
     */
    public static List<LivestockResourceThreeAssetsProblemListItemVo> buildThreeAssetsProblemListItems(
            List<BizThreeAssetsProblemReport> rows)
    {
        if (rows == null || rows.isEmpty())
        {
            return Collections.emptyList();
        }
        List<LivestockResourceThreeAssetsProblemListItemVo> result = new ArrayList<>(rows.size());
        for (BizThreeAssetsProblemReport row : rows)
        {
            if (row == null)
            {
                continue;
            }
            LivestockResourceThreeAssetsProblemListItemVo item = new LivestockResourceThreeAssetsProblemListItemVo();
            item.setReportTime(row.getReportTime());
            item.setTownName(row.getTownName());
            item.setVillageName(row.getVillageName());
            item.setProblemLevel(row.getProblemLevel());
            item.setProblemLevelName(resolveProblemLevelName(row.getProblemLevel()));
            item.setProblemDetail(row.getProblemDetail());
            item.setHandleResult(row.getHandleResult());
            result.add(item);
        }
        return result;
    }

    /** 问题等级编码 → 展示名：general/key/major → 一般/重点/重大 */
    public static String resolveProblemLevelName(String levelCode)
    {
        if (LivestockResourceScreenRules.PROBLEM_LEVEL_GENERAL.equals(levelCode))
        {
            return "一般";
        }
        if (LivestockResourceScreenRules.PROBLEM_LEVEL_KEY.equals(levelCode))
        {
            return "重点";
        }
        if (LivestockResourceScreenRules.PROBLEM_LEVEL_MAJOR.equals(levelCode))
        {
            return "重大";
        }
        return levelCode;
    }

    /**
     * 畜牧产品产量 → 明细分页 VO；肉/奶产量统一 4 位小数。
     *
     * @param rows {@code biz_livestock_product_output} 列表
     */
    public static List<LivestockResourceProductOutputListItemVo> buildProductOutputListItems(
            List<BizLivestockProductOutput> rows)
    {
        if (rows == null || rows.isEmpty())
        {
            return Collections.emptyList();
        }
        List<LivestockResourceProductOutputListItemVo> result = new ArrayList<>(rows.size());
        for (BizLivestockProductOutput row : rows)
        {
            if (row == null)
            {
                continue;
            }
            LivestockResourceProductOutputListItemVo item = new LivestockResourceProductOutputListItemVo();
            item.setStatYear(row.getStatYear());
            item.setStatMonth(row.getStatMonth());
            item.setTownName(row.getTownName());
            item.setVillageName(row.getVillageName());
            item.setMeatOutput(scaleOutput(row.getMeatOutput()));
            item.setMilkOutput(scaleOutput(row.getMilkOutput()));
            result.add(item);
        }
        return result;
    }

    /**
     * 疫苗接种 → 明细分页 VO；免疫率见 {@link #resolveRowImmunityRate}。
     *
     * @param rows {@code biz_vaccination_data} 列表
     */
    public static List<LivestockResourceVaccinationListItemVo> buildVaccinationListItems(
            List<BizVaccinationData> rows)
    {
        if (rows == null || rows.isEmpty())
        {
            return Collections.emptyList();
        }
        List<LivestockResourceVaccinationListItemVo> result = new ArrayList<>(rows.size());
        for (BizVaccinationData row : rows)
        {
            if (row == null)
            {
                continue;
            }
            LivestockResourceVaccinationListItemVo item = new LivestockResourceVaccinationListItemVo();
            item.setStatYear(row.getStatYear());
            item.setStatMonth(row.getStatMonth());
            item.setTownName(row.getTownName());
            item.setVillageName(row.getVillageName());
            item.setShouldVaccinateCount(row.getShouldVaccinateCount());
            item.setActualVaccinateCount(row.getActualVaccinateCount());
            item.setImmunityRate(resolveRowImmunityRate(row));
            result.add(item);
        }
        return result;
    }

    /** 单行免疫率：优先用落库值；为空则按应免/实免重算（应免≤0 时 0.00） */
    public static BigDecimal resolveRowImmunityRate(BizVaccinationData row)
    {
        if (row != null && row.getImmunityRate() != null)
        {
            return row.getImmunityRate().setScale(2, RoundingMode.HALF_UP);
        }
        int should = row != null && row.getShouldVaccinateCount() != null ? row.getShouldVaccinateCount() : 0;
        int actual = row != null && row.getActualVaccinateCount() != null ? row.getActualVaccinateCount() : 0;
        if (should <= 0)
        {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        return BigDecimal.valueOf(actual)
                .multiply(BigDecimal.valueOf(100))
                .divide(BigDecimal.valueOf(should), 2, RoundingMode.HALF_UP);
    }

    /** 畜牧产品产量：1～maxMonth 肉/奶对比；无最大月则空序列 */
    public static LivestockResourceProductOutputTrendVo buildProductOutputTrend(
            Integer maxMonth, List<LivestockResourceProductOutputMonthDto> rows)
    {
        LivestockResourceProductOutputTrendVo vo = new LivestockResourceProductOutputTrendVo();
        if (maxMonth == null || maxMonth < 1)
        {
            vo.setMaxMonth(null);
            vo.setMonthlySeries(Collections.emptyList());
            return vo;
        }
        int end = Math.min(maxMonth, 12);
        Map<Integer, LivestockResourceProductOutputMonthDto> map = new HashMap<>();
        if (rows != null)
        {
            for (LivestockResourceProductOutputMonthDto row : rows)
            {
                if (row != null && row.getStatMonth() != null)
                {
                    map.put(row.getStatMonth(), row);
                }
            }
        }
        List<LivestockResourceProductOutputMonthPointVo> series = new ArrayList<>();
        for (int m = 1; m <= end; m++)
        {
            LivestockResourceProductOutputMonthDto row = map.get(m);
            LivestockResourceProductOutputMonthPointVo point = new LivestockResourceProductOutputMonthPointVo();
            point.setMonth(m);
            point.setMeatOutput(scaleOutput(row != null ? row.getMeatOutput() : null));
            point.setMilkOutput(scaleOutput(row != null ? row.getMilkOutput() : null));
            series.add(point);
        }
        vo.setMaxMonth(end);
        vo.setMonthlySeries(series);
        return vo;
    }

    /** 疫苗接种区块 */
    public static LivestockResourceVaccinationOverviewVo buildVaccinationOverview(
            Integer statMonth, LivestockResourceVaccinationSumDto vaccination)
    {
        LivestockResourceVaccinationOverviewVo vo = new LivestockResourceVaccinationOverviewVo();
        vo.setStatMonth(statMonth);
        vo.setImmunityRate(calcImmunityRate(vaccination));
        vo.setShouldVaccinateCount(vaccination != null ? safeLong(vaccination.getShouldVaccinateCount()) : 0L);
        vo.setActualVaccinateCount(vaccination != null ? safeLong(vaccination.getActualVaccinateCount()) : 0L);
        return vo;
    }

    /**
     * 科技特派员双层饼：党员/其他、男性/女性。
     * <p>其他 = max(0, 总人数 - 党员)；总人数优先取 envoy_total_count，否则取男女之和。</p>
     */
    public static LivestockResourceTechEnvoyStructureVo buildTechEnvoyStructure(
            LivestockResourceTechPersonnelDto raw)
    {
        long male = raw != null ? safeLong(raw.getEnvoyMaleCount()) : 0L;
        long female = raw != null ? safeLong(raw.getEnvoyFemaleCount()) : 0L;
        long party = raw != null ? safeLong(raw.getEnvoyPartyMemberCount()) : 0L;
        long total = raw != null ? safeLong(raw.getEnvoyTotalCount()) : 0L;
        if (total <= 0)
        {
            total = male + female;
        }
        if (party > total)
        {
            party = total;
        }
        long other = Math.max(0L, total - party);

        LivestockResourceTechEnvoyStructureVo vo = new LivestockResourceTechEnvoyStructureVo();
        vo.setTotalCount(total);
        List<LivestockResourceShareItemVo> partyShare = new ArrayList<>();
        partyShare.add(shareItem("PARTY", "党员", party, total));
        partyShare.add(shareItem("OTHER", "其他", other, total));
        vo.setPartyShare(partyShare);
        List<LivestockResourceShareItemVo> genderShare = new ArrayList<>();
        genderShare.add(shareItem("MALE", "男性", male, total > 0 ? total : male + female));
        genderShare.add(shareItem("FEMALE", "女性", female, total > 0 ? total : male + female));
        vo.setGenderShare(genderShare);
        return vo;
    }

    /**
     * 农业事业单位职级柱图：员级、初级、中级、高级、管理级。
     */
    public static LivestockResourceAgriUnitStructureVo buildAgriUnitStructure(
            LivestockResourceTechPersonnelDto raw)
    {
        long clerk = raw != null ? safeLong(raw.getAgriClerkCount()) : 0L;
        long junior = raw != null ? safeLong(raw.getAgriJuniorCount()) : 0L;
        long intermediate = raw != null ? safeLong(raw.getAgriIntermediateCount()) : 0L;
        long senior = raw != null ? safeLong(raw.getAgriAssociateSeniorCount()) : 0L;
        long management = raw != null ? safeLong(raw.getAgriManagementCount()) : 0L;
        long total = raw != null ? safeLong(raw.getAgriTotalCount()) : 0L;
        long sumRanks = clerk + junior + intermediate + senior + management;
        if (total <= 0)
        {
            total = sumRanks;
        }

        LivestockResourceAgriUnitStructureVo vo = new LivestockResourceAgriUnitStructureVo();
        vo.setTotalCount(total);
        List<LivestockResourceShareItemVo> items = new ArrayList<>();
        items.add(shareItem("CLERK", "员级", clerk, total));
        items.add(shareItem("JUNIOR", "初级", junior, total));
        items.add(shareItem("INTERMEDIATE", "中级", intermediate, total));
        items.add(shareItem("SENIOR", "高级", senior, total));
        items.add(shareItem("MANAGEMENT", "管理级", management, total));
        vo.setRankItems(items);
        return vo;
    }

    private static LivestockResourceShareItemVo shareItem(String code, String name, long count, long total)
    {
        LivestockResourceShareItemVo item = new LivestockResourceShareItemVo();
        item.setItemCode(code);
        item.setItemName(name);
        item.setCount(count);
        item.setRatio(ratioPercent(count, total));
        return item;
    }

    /** 免疫率 = (实免数和 / 应免数和) × 100，保留 2 位；应免和为 0 时返回 0.00 */
    public static BigDecimal calcImmunityRate(LivestockResourceVaccinationSumDto vaccination)
    {
        long should = vaccination != null ? safeLong(vaccination.getShouldVaccinateCount()) : 0L;
        long actual = vaccination != null ? safeLong(vaccination.getActualVaccinateCount()) : 0L;
        if (should <= 0)
        {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        return BigDecimal.valueOf(actual)
                .multiply(BigDecimal.valueOf(100))
                .divide(BigDecimal.valueOf(should), 2, RoundingMode.HALF_UP);
    }

    private static BigDecimal scaleMoney(BigDecimal value)
    {
        return value != null ? value.setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
    }

    private static BigDecimal scaleOutput(BigDecimal value)
    {
        return value != null ? value.setScale(4, RoundingMode.HALF_UP) : BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP);
    }

    public static LivestockResourceActivityTrendVo buildActivityTrend(
            List<LivestockResourceDateCountDto> sessionRows,
            List<LivestockResourceDateCountDto> questionRows,
            LocalDate statDate)
    {
        Map<String, Long> sessions = toCountMap(sessionRows);
        Map<String, Long> questions = toCountMap(questionRows);
        List<LivestockResourceDailyPointVo> series = new ArrayList<>();
        for (int i = LivestockResourceScreenRules.ACTIVITY_DAYS - 1; i >= 0; i--)
        {
            LocalDate day = statDate.minusDays(i);
            String key = day.format(DATE_FMT);
            LivestockResourceDailyPointVo point = new LivestockResourceDailyPointVo();
            point.setDate(key);
            point.setSessionCount(sessions.getOrDefault(key, 0L));
            point.setQuestionCount(questions.getOrDefault(key, 0L));
            series.add(point);
        }
        LivestockResourceActivityTrendVo vo = new LivestockResourceActivityTrendVo();
        vo.setDailySeries(series);
        return vo;
    }

    public static LivestockResourceUserStructureVo buildUserStructure(LivestockResourceUserStructureDto raw)
    {
        long newCount = raw != null && raw.getNewUserCount() != null ? raw.getNewUserCount() : 0L;
        long oldCount = raw != null && raw.getOldUserCount() != null ? raw.getOldUserCount() : 0L;
        LivestockResourceUserStructureVo vo = new LivestockResourceUserStructureVo();
        vo.setNewUserCount(newCount);
        vo.setOldUserCount(oldCount);
        vo.setNewUserRatio(ratioPercent(newCount, newCount + oldCount));
        vo.setOldUserRatio(ratioPercent(oldCount, newCount + oldCount));
        return vo;
    }

    public static LivestockResourceCategoryShareVo buildCategoryShare(
            List<LivestockResourceCategoryCountDto> rows, long totalAskerMessages)
    {
        Map<String, Long> countByCode = new HashMap<>();
        if (rows != null)
        {
            for (LivestockResourceCategoryCountDto row : rows)
            {
                if (row != null && row.getCategoryCode() != null)
                {
                    countByCode.put(normalizeCategoryCode(row.getCategoryCode()),
                            safeLong(row.getCount()));
                }
            }
        }
        List<LivestockResourceCategoryItemVo> items = buildFixedCategoryItems(countByCode, totalAskerMessages);
        LivestockResourceCategoryShareVo vo = new LivestockResourceCategoryShareVo();
        vo.setTotalAskerMessages(totalAskerMessages);
        vo.setItems(items);
        return vo;
    }

    public static List<LivestockResourceCategoryItemVo> buildModelCallAnalysis(
            List<LivestockResourceCategoryItemVo> items)
    {
        if (items == null)
        {
            return Collections.emptyList();
        }
        List<LivestockResourceCategoryItemVo> copy = new ArrayList<>();
        for (LivestockResourceCategoryItemVo item : items)
        {
            LivestockResourceCategoryItemVo row = new LivestockResourceCategoryItemVo();
            row.setCategoryCode(item.getCategoryCode());
            row.setCategoryName(item.getCategoryName());
            row.setCount(item.getCount());
            copy.add(row);
        }
        return copy;
    }

    public static List<LivestockResourceCategoryItemVo> buildHotTopics(
            List<LivestockResourceCategoryCountDto> rows, int topN)
    {
        if (rows == null || rows.isEmpty())
        {
            return Collections.emptyList();
        }
        List<LivestockResourceCategoryCountDto> sorted = new ArrayList<>(rows);
        sorted.sort(Comparator.comparingLong((LivestockResourceCategoryCountDto r) -> safeLong(r.getCount())).reversed()
                .thenComparing(LivestockResourceCategoryCountDto::getLastEventTime,
                        Comparator.nullsLast(Comparator.reverseOrder()))
                .thenComparing(r -> normalizeCategoryCode(r.getCategoryCode())));
        int limit = Math.min(topN, sorted.size());
        List<LivestockResourceCategoryItemVo> result = new ArrayList<>();
        for (int i = 0; i < limit; i++)
        {
            LivestockResourceCategoryCountDto row = sorted.get(i);
            LivestockResourceCategoryItemVo item = new LivestockResourceCategoryItemVo();
            String code = normalizeCategoryCode(row.getCategoryCode());
            item.setCategoryCode(code);
            item.setCategoryName(resolveCategoryName(code));
            item.setCount(safeLong(row.getCount()));
            result.add(item);
        }
        return result;
    }

    /** 距 anchor 向前共 24 个整点小时桶（含 anchor 所在小时）。 */
    public static LivestockResourceHourlyHeatVo buildHourlyHeat(
            List<LivestockResourceDateCountDto> sessionRows,
            List<LivestockResourceDateCountDto> questionRows,
            LocalDateTime anchor)
    {
        LocalDateTime floorAnchor = anchor.truncatedTo(ChronoUnit.HOURS);
        Map<String, Long> sessions = toDateHourBucketMap(sessionRows);
        Map<String, Long> questions = toDateHourBucketMap(questionRows);
        List<LivestockResourceHourlyPointVo> series = new ArrayList<>();
        LocalDateTime start = floorAnchor.minusHours(LivestockResourceScreenRules.HOURLY_BUCKETS - 1);
        for (int slot = 0; slot < LivestockResourceScreenRules.HOURLY_BUCKETS; slot++)
        {
            LocalDateTime bucket = start.plusHours(slot);
            String key = bucket.format(HOUR_BUCKET_KEY_FMT);
            LivestockResourceHourlyPointVo point = new LivestockResourceHourlyPointVo();
            point.setBucketStart(bucket.format(HOUR_FMT));
            point.setBucketDateTime(bucket.format(HOUR_BUCKET_TOOLTIP_FMT));
            point.setSessionCount(sessions.getOrDefault(key, 0L));
            point.setQuestionCount(questions.getOrDefault(key, 0L));
            series.add(point);
        }
        LivestockResourceHourlyHeatVo vo = new LivestockResourceHourlyHeatVo();
        vo.setWindowEnd(floorAnchor.format(HOUR_BUCKET_TOOLTIP_FMT));
        vo.setHourlySeries(series);
        return vo;
    }

    public static List<LivestockResourceSessionBandVo> buildSessionReplyDistribution(
            List<LivestockResourceBandCountDto> rows, long yearSessionTotal)
    {
        Map<String, Long> byBand = new HashMap<>();
        if (rows != null)
        {
            for (LivestockResourceBandCountDto row : rows)
            {
                if (row != null && row.getBandCode() != null)
                {
                    byBand.put(row.getBandCode(), safeLong(row.getSessionCount()));
                }
            }
        }
        List<LivestockResourceSessionBandVo> list = new ArrayList<>();
        for (String[] band : BAND_ORDER)
        {
            long count = byBand.getOrDefault(band[0], 0L);
            LivestockResourceSessionBandVo vo = new LivestockResourceSessionBandVo();
            vo.setBandCode(band[0]);
            vo.setBandName(band[1]);
            vo.setSessionCount(count);
            vo.setRatio(ratioPercent(count, yearSessionTotal));
            list.add(vo);
        }
        return list;
    }

    public static LivestockResourceReplyStatusVo buildReplyStatus(long okCount, long failCount)
    {
        long total = okCount + failCount;
        LivestockResourceStatusItemVo ok = statusItem(
                LivestockResourceScreenRules.STATUS_OK, "正常应答", okCount, total);
        LivestockResourceStatusItemVo fail = statusItem(
                LivestockResourceScreenRules.STATUS_FAIL, "调用失败", failCount, total);
        LivestockResourceReplyStatusVo vo = new LivestockResourceReplyStatusVo();
        vo.setTotalAiReplies(total);
        vo.setItems(Arrays.asList(ok, fail));
        return vo;
    }

    /** 对提问文本做中英文 2+ 字分词，去停用词后按词频取 Top N。 */
    public static List<LivestockResourceKeywordVo> extractTopKeywords(List<String> texts, int topN)
    {
        if (texts == null || texts.isEmpty())
        {
            return Collections.emptyList();
        }
        Map<String, Integer> freq = new LinkedHashMap<>();
        for (String text : texts)
        {
            if (text == null || text.trim().isEmpty())
            {
                continue;
            }
            collectTokens(text, CN_TOKEN, freq);
            collectTokens(text, EN_TOKEN, freq);
        }
        List<Map.Entry<String, Integer>> entries = new ArrayList<>(freq.entrySet());
        entries.sort(Comparator.<Map.Entry<String, Integer>>comparingInt(Map.Entry::getValue).reversed()
                .thenComparing(Map.Entry::getKey));
        List<LivestockResourceKeywordVo> result = new ArrayList<>();
        int limit = Math.min(topN, entries.size());
        for (int i = 0; i < limit; i++)
        {
            Map.Entry<String, Integer> e = entries.get(i);
            LivestockResourceKeywordVo vo = new LivestockResourceKeywordVo();
            vo.setKeyword(e.getKey());
            vo.setCount(e.getValue().longValue());
            result.add(vo);
        }
        return result;
    }

    public static String resolveCategoryName(String categoryCode)
    {
        String code = normalizeCategoryCode(categoryCode);
        for (String[] row : CATEGORY_ORDER)
        {
            if (row[0].equals(code))
            {
                return row[1];
            }
        }
        if ("1".equals(code))
        {
            return "养殖技巧";
        }
        if ("2".equals(code))
        {
            return "疾病诊疗";
        }
        if ("3".equals(code))
        {
            return "用药指导";
        }
        if ("4".equals(code))
        {
            return "设备操作";
        }
        return "未分类";
    }

    private static List<LivestockResourceCategoryItemVo> buildFixedCategoryItems(
            Map<String, Long> countByCode, long total)
    {
        List<LivestockResourceCategoryItemVo> items = new ArrayList<>();
        for (String[] row : CATEGORY_ORDER)
        {
            long count = countByCode.getOrDefault(row[0], 0L);
            LivestockResourceCategoryItemVo item = new LivestockResourceCategoryItemVo();
            item.setCategoryCode(row[0]);
            item.setCategoryName(row[1]);
            item.setCount(count);
            item.setRatio(ratioPercent(count, total));
            items.add(item);
        }
        return items;
    }

    private static String normalizeCategoryCode(String raw)
    {
        if (raw == null || raw.trim().isEmpty())
        {
            return LivestockResourceScreenRules.CATEGORY_UNCAT;
        }
        String code = raw.trim();
        if (LivestockResourceScreenRules.CATEGORY_UNCAT.equalsIgnoreCase(code))
        {
            return LivestockResourceScreenRules.CATEGORY_UNCAT;
        }
        if ("1".equals(code) || String.valueOf(AiConsultConstants.CATEGORY_BREEDING).equals(code))
        {
            return "1";
        }
        if ("2".equals(code) || String.valueOf(AiConsultConstants.CATEGORY_DISEASE).equals(code))
        {
            return "2";
        }
        if ("3".equals(code) || String.valueOf(AiConsultConstants.CATEGORY_MEDICINE).equals(code))
        {
            return "3";
        }
        if ("4".equals(code) || String.valueOf(AiConsultConstants.CATEGORY_EQUIPMENT).equals(code))
        {
            return "4";
        }
        return LivestockResourceScreenRules.CATEGORY_UNCAT;
    }

    private static void collectTokens(String text, Pattern pattern, Map<String, Integer> freq)
    {
        Matcher matcher = pattern.matcher(text);
        while (matcher.find())
        {
            String token = matcher.group().toLowerCase();
            if (token.length() < 2 || STOP_WORDS.contains(token))
            {
                continue;
            }
            freq.merge(token, 1, Integer::sum);
        }
    }

    private static Map<String, Long> toCountMap(List<LivestockResourceDateCountDto> rows)
    {
        Map<String, Long> map = new HashMap<>();
        if (rows != null)
        {
            for (LivestockResourceDateCountDto row : rows)
            {
                if (row != null && row.getBucketKey() != null)
                {
                    map.put(row.getBucketKey(), safeLong(row.getCount()));
                }
            }
        }
        return map;
    }

    private static Map<String, Long> toDateHourBucketMap(List<LivestockResourceDateCountDto> rows)
    {
        Map<String, Long> map = new HashMap<>();
        if (rows != null)
        {
            for (LivestockResourceDateCountDto row : rows)
            {
                if (row != null && row.getBucketKey() != null)
                {
                    map.put(row.getBucketKey().trim(), safeLong(row.getCount()));
                }
            }
        }
        return map;
    }

    private static LivestockResourceStatusItemVo statusItem(String code, String name, long count, long total)
    {
        LivestockResourceStatusItemVo vo = new LivestockResourceStatusItemVo();
        vo.setStatusCode(code);
        vo.setStatusName(name);
        vo.setCount(count);
        vo.setRatio(ratioPercent(count, total));
        return vo;
    }

    private static BigDecimal ratioPercent(long part, long total)
    {
        if (total <= 0)
        {
            return BigDecimal.ZERO.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
        }
        return BigDecimal.valueOf(part * 100.0 / total).setScale(RATIO_SCALE, RoundingMode.HALF_UP);
    }

    private static BigDecimal toAvgSeconds(Double avgMs)
    {
        if (avgMs == null)
        {
            return BigDecimal.ZERO.setScale(RATIO_SCALE, RoundingMode.HALF_UP);
        }
        return BigDecimal.valueOf(avgMs / 1000.0).setScale(RATIO_SCALE, RoundingMode.HALF_UP);
    }

    private static long safeLong(Long value)
    {
        return value != null ? value : 0L;
    }
}
