package com.gzzm.lobster.api;

import com.gzzm.lobster.common.JsonUtil;
import com.gzzm.platform.commons.Tools;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * KbMockServlet —— 知识库 mock 服务（KB API v1 实现）/ Local KB mock for end-to-end testing.
 *
 * <p>主题：中国人口变化 · 人口政策 · 人口风险 · 地方实践（15 篇文档 / 4 个 scope）.
 *
 * <h3>使用方法</h3>
 * <ol>
 *   <li>web.xml 已挂在 {@code /ai/api/_mock/kb/*}（同时支持 GET / POST）</li>
 *   <li>编辑 {@code lobster.xml}：
 *       {@code <oaKnowledgeBaseUrl>http://localhost:8080/ai/api/_mock/kb</oaKnowledgeBaseUrl>}
 *       （把 host:port 换成你 zm-ai-server 实际地址，路径必须以 {@code /_mock/kb} 结尾）</li>
 *   <li>重启 zm-ai-server</li>
 *   <li>前端打开知识库开关 → 提问「2024 年中国出生人口」/「老龄化对养老金的影响」/「三孩政策怎么落地」</li>
 *   <li>tool 卡下方应出现引用气泡，hover 看到 snippet + scope + 相关度</li>
 * </ol>
 *
 * <h3>路径契约（与 OaKnowledgeClient javadoc 对齐 + mock 扩展）</h3>
 * <ul>
 *   <li>GET  {base}/scopes —— 列出 4 个范围</li>
 *   <li>GET  {base}/scopes/{scopeId} —— <b>mock 扩展</b>：单 scope 详情 + 其下文档目录
 *       （含 docId/title/snippet/updatedAt，不含 content；正文需 /detail 单独取）。
 *       用于浏览测试语料 / 未来"分类浏览"UI 起点</li>
 *   <li>POST {base}/search —— body: {query, maxResults?, scopeIds?[], userId?, deptId?, orgId?}</li>
 *   <li>POST {base}/detail —— body: {docId, ...}</li>
 * </ul>
 *
 * <p>评分：title 命中 ×3 + snippet 命中 ×2 + content 命中 ×1，按字符级 substring。
 * 不接 jieba —— mock 用关键字命中即足够展示功能。
 *
 * <p>权限：mock 不裁决，所有 user 都能访问全部文档（生产 KB 才需要按 X-User-Id 过滤）。
 */
public class KbMockServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        dispatch(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        dispatch(req, resp);
    }

    private void dispatch(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String uri = req.getRequestURI();
        // 兼容 contextPath / 多段前缀，按尾部识别.
        // 注意 /scopes/{id} 必须在 endsWith("/scopes") 之前判 —— 否则不会撞，但语义上更清楚.
        try {
            if (uri.endsWith("/scopes")) {
                writeJson(resp, 200, doScopes());
            } else if (uri.contains("/scopes/")) {
                String scopeId = uri.substring(uri.indexOf("/scopes/") + "/scopes/".length());
                // 兼容尾随 / —— 例如 /scopes/policy/ 这种把斜杠手抖带上的请求
                if (scopeId.endsWith("/")) scopeId = scopeId.substring(0, scopeId.length() - 1);
                if (scopeId.isEmpty()) {
                    writeJson(resp, 400, errorBody("bad_request", "scope id required"));
                } else {
                    Map<String, Object> result = doScope(scopeId);
                    if (result == null) {
                        writeJson(resp, 404, errorBody("not_found", "scope not found: " + scopeId));
                    } else {
                        writeJson(resp, 200, result);
                    }
                }
            } else if (uri.endsWith("/search")) {
                writeJson(resp, 200, doSearch(readBody(req)));
            } else if (uri.endsWith("/detail")) {
                writeJson(resp, 200, doDetail(readBody(req)));
            } else {
                writeJson(resp, 404, errorBody("not_found", "unknown KB path: " + uri));
            }
        } catch (Throwable t) {
            try { Tools.log("[KbMockServlet] dispatch failed for " + uri, t); }
            catch (Throwable ignore) { /* ignore */ }
            writeJson(resp, 500, errorBody("upstream", safeMsg(t)));
        }
    }

    // ============================================================
    // /scopes
    // ============================================================

    private Map<String, Object> doScopes() {
        List<Map<String, Object>> rows = new ArrayList<>();
        for (String[] s : SCOPES) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("id", s[0]);
            row.put("name", s[1]);
            row.put("description", s[2]);
            row.put("docCount", countDocsInScope(s[0]));
            rows.add(row);
        }
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("scopes", rows);
        return out;
    }

    private long countDocsInScope(String scopeId) {
        long n = 0;
        for (Doc d : DOCS) if (d.scope.equals(scopeId)) n++;
        return n;
    }

    // ============================================================
    // /scopes/{scopeId} —— 单 scope 详情 + 该 scope 下文档目录
    // ============================================================

    /**
     * 返回 scope 详情 + 其下所有文档的轻量索引（不含 content，正文需 /detail 单独取）。
     * scope 不存在返 null，由 dispatch 层落 404.
     */
    private Map<String, Object> doScope(String scopeId) {
        String[] info = null;
        for (String[] s : SCOPES) {
            if (s[0].equals(scopeId)) { info = s; break; }
        }
        if (info == null) return null;

        Map<String, Object> out = new LinkedHashMap<>();
        out.put("id", info[0]);
        out.put("name", info[1]);
        out.put("description", info[2]);

        List<Map<String, Object>> docs = new ArrayList<>();
        for (Doc d : DOCS) {
            if (!d.scope.equals(scopeId)) continue;
            Map<String, Object> r = new LinkedHashMap<>();
            r.put("docId", d.docId);
            r.put("title", d.title);
            r.put("snippet", d.snippet);
            r.put("updatedAt", d.updatedAt);
            docs.add(r);
        }
        out.put("docs", docs);
        out.put("docCount", docs.size());
        return out;
    }

    // ============================================================
    // /search
    // ============================================================

    @SuppressWarnings("unchecked")
    private Map<String, Object> doSearch(Map<String, Object> body) {
        long t0 = System.currentTimeMillis();
        String query = body == null ? "" : asStr(body.get("query"));
        int maxResults = clamp(asInt(body == null ? null : body.get("maxResults"), 10), 1, 20);
        List<String> scopeIds = asStringList(body == null ? null : body.get("scopeIds"));

        // 按 scope 过滤 → 评分 → 排序 → 截断
        List<Object[]> scored = new ArrayList<>();
        if (query != null && !query.trim().isEmpty()) {
            String[] terms = tokenize(query);
            for (Doc d : DOCS) {
                if (scopeIds != null && !scopeIds.isEmpty() && !scopeIds.contains(d.scope)) continue;
                int score = scoreOne(d, terms);
                if (score > 0) scored.add(new Object[]{ d, score });
            }
            scored.sort((a, b) -> Integer.compare((Integer) b[1], (Integer) a[1]));
        }

        List<Map<String, Object>> hits = new ArrayList<>();
        int max = Math.min(maxResults, scored.size());
        for (int i = 0; i < max; i++) {
            Doc d = (Doc) scored.get(i)[0];
            int s   = (Integer) scored.get(i)[1];
            // 把原始 score 归一化到 0..1，方便前端做相关度展示
            double normalized = Math.min(1.0, s / 12.0);
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("docId", d.docId);
            row.put("title", d.title);
            row.put("snippet", d.snippet);
            row.put("score", round2(normalized));
            row.put("source", d.scope);
            row.put("scopeName", scopeName(d.scope));
            // 不再生成 mock.kb.local 假外链——前端引用气泡点击直接走详情抽屉（KB API /detail）
            row.put("updatedAt", d.updatedAt);
            hits.add(row);
        }
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("hits", hits);
        out.put("tookMs", System.currentTimeMillis() - t0);
        return out;
    }

    private static int scoreOne(Doc d, String[] terms) {
        int total = 0;
        String t = d.title == null ? "" : d.title;
        String sn = d.snippet == null ? "" : d.snippet;
        String c = d.content == null ? "" : d.content;
        for (String term : terms) {
            if (term == null || term.isEmpty()) continue;
            total += countOccurrences(t,  term) * 3;
            total += countOccurrences(sn, term) * 2;
            total += countOccurrences(c,  term) * 1;
        }
        return total;
    }

    private static int countOccurrences(String haystack, String needle) {
        if (haystack.isEmpty() || needle.isEmpty()) return 0;
        int n = 0, idx = 0;
        while ((idx = haystack.indexOf(needle, idx)) >= 0) {
            n++;
            idx += needle.length();
        }
        return n;
    }

    /**
     * 分词器（mock 级）/ Tokenizer (mock-grade).
     * 中文：按 2-gram 切分（"中国人口" → "中国","国人","人口","中国人口"）；
     * 英文/数字：按空格 + 标点切。
     * 这套足以让命中带一些层次（精确短语得分高于零散字），不强求语义级匹配.
     */
    private static String[] tokenize(String q) {
        if (q == null) return new String[0];
        String s = q.trim();
        if (s.isEmpty()) return new String[0];
        List<String> out = new ArrayList<>();
        // 1) 完整 query 自身（最高权重命中）
        out.add(s);
        // 2) 按非中文/数字/字母拆段
        for (String seg : s.split("[\\s,，。.;；:：!！?？、/\\\\\"'()（）\\[\\]【】<>《》-]+")) {
            if (seg.isEmpty()) continue;
            if (seg.length() <= 4) {
                out.add(seg);
            } else {
                // 中文长串：2-gram 滑窗
                for (int i = 0; i + 2 <= seg.length(); i++) out.add(seg.substring(i, i + 2));
                out.add(seg);
            }
        }
        return out.toArray(new String[0]);
    }

    // ============================================================
    // /detail
    // ============================================================

    private Map<String, Object> doDetail(Map<String, Object> body) {
        String docId = body == null ? null : asStr(body.get("docId"));
        Doc d = findDoc(docId);
        if (d == null) {
            return errorBody("not_found", "doc not found: " + docId);
        }
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("docId", d.docId);
        out.put("title", d.title);
        out.put("content", d.content);
        out.put("source", d.scope);
        out.put("scopeName", scopeName(d.scope));
        out.put("url", "https://mock.kb.local/doc/" + d.docId);
        out.put("updatedAt", d.updatedAt);
        return out;
    }

    private static Doc findDoc(String docId) {
        if (docId == null) return null;
        for (Doc d : DOCS) if (d.docId.equals(docId)) return d;
        return null;
    }

    // ============================================================
    // 数据集：中国人口变化 / 政策 / 风险 / 地方实践
    // 字段：docId, scope, title, snippet, content (markdown), updatedAt
    //
    // 注意：所有数字仅作演示用——是基于公开报道整理的近似值，不做权威依据.
    // ============================================================

    /** Scope 三元组：[id, name, description]. */
    private static final String[][] SCOPES = new String[][] {
            { "policy", "人口政策", "国家及部委关于生育、计生、养老的政策法规与配套措施" },
            { "data",   "人口数据", "国家统计局、普查、年报等权威人口统计与趋势" },
            { "risk",   "人口风险", "老龄化、出生断崖、劳动力短缺等结构性挑战分析" },
            { "case",   "地方实践", "各省市生育激励、托育、养老等典型试点案例" },
    };

    private static String scopeName(String id) {
        for (String[] s : SCOPES) if (s[0].equals(id)) return s[1];
        return id;
    }

    private static final List<Doc> DOCS = Arrays.asList(
        // ============ policy ============
        new Doc("doc_p01", "policy",
            "三孩生育政策实施意见（2021）",
            "中央决定实施一对夫妻可生育三个子女的政策，配套生育、养育、教育成本降低措施，包括育儿假、托育服务、住房与税收支持。",
            "# 三孩生育政策实施意见（中发〔2021〕30 号）\n\n## 背景\n2021 年 5 月，中央政治局召开会议，审议《关于优化生育政策促进人口长期均衡发展的决定》。" +
            "鉴于第七次人口普查显示总和生育率已降至 1.3，65 岁以上人口占比 13.5%，决定将原「全面二孩」扩展为三孩政策。\n\n" +
            "## 主要内容\n- 取消社会抚养费、清理与三孩政策不衔接的配套法规\n- 建立健全婚嫁、生育、养育、教育的一体化支持体系\n" +
            "- 发展普惠托育服务，到 2025 年每千人口拥有 3 岁以下婴幼儿托位数 4.5 个\n- 完善生育假、育儿假等假期制度，配偶陪产假各省差异化执行\n" +
            "- 个人所得税专项附加扣除新增 3 岁以下婴幼儿照护，每孩每月 1000 元（2022 年起施行，后调整至 2000 元）\n\n" +
            "## 落地节奏\n- 2021 年 8 月《人口与计划生育法》修订通过，正式取消社会抚养费\n- 各省自 2021 年下半年陆续修订地方计生条例\n" +
            "- 2024 年配套国家育儿补贴方案出台（见 doc_p02）",
            "2021-05-31"),

        new Doc("doc_p02", "policy",
            "2024 年国家育儿补贴方案",
            "中央财政对生育二孩、三孩家庭按月发放育儿补贴；多地在国家标准基础上上浮，部分省市对一孩同样纳入补贴。",
            "# 2024 国家育儿补贴方案\n\n## 总体设计\n2024 年 3 月，国务院办公厅印发《关于加快建设育儿友好型社会的若干措施》，" +
            "首次在国家层面建立面向 0-3 岁婴幼儿的现金育儿补贴制度。\n\n## 补贴标准（基础线）\n- 一孩：地方自定（多数省份未纳入）\n" +
            "- 二孩：每月 500 元\n- 三孩及以上：每月 1000 元\n- 发放期至子女满 3 周岁\n\n## 地方加码\n- 上海：一孩 2000、二孩 5000、三孩 1 万元（年度，见 doc_c01）\n" +
            "- 杭州：二孩 5000、三孩 2 万元（一次性 + 月补）\n- 沈阳：三孩每月 500 元（叠加在国家标准之上）\n\n" +
            "## 资金来源\n中央与地方按 6:4 分担基础线，地方加码部分由省级财政自筹.",
            "2024-03-15"),

        new Doc("doc_p03", "policy",
            "婚育假期与产假改革",
            "全国 31 省市自 2021 年起延长产假至 158-188 天；配偶陪产假 15-30 天；新增育儿假 5-15 天，子女满 3 周岁前每年享有.",
            "# 婚育假期与产假改革\n\n## 背景\n配套三孩政策，国家鼓励各省延长生育相关假期、扩大配偶与父母双方共担育儿成本。\n\n" +
            "## 各项假期（截至 2024 年）\n| 假期类型 | 国家最低 | 主流省份 | 上限 |\n|---|---|---|---|\n" +
            "| 产假 | 98 天 | 158 天 | 188 天（重庆）|\n| 配偶陪产假 | 无强制 | 15 天 | 30 天（甘肃）|\n" +
            "| 育儿假 | 无 | 5-10 天/年 | 15 天/年（江苏）|\n| 婚假 | 3 天 | 13 天 | 30 天（山西）|\n\n" +
            "## 实施争议\n- 中小企业承担女性就业成本上升，部分地区出现「招聘性别歧视」上升\n- 配偶陪产假实际使用率不足 50%（2023 年人社部抽样）\n" +
            "- 学界呼吁将假期成本由生育保险基金而非企业直接承担",
            "2023-08-10"),

        new Doc("doc_p04", "policy",
            "中国计划生育政策的历史演变（1980-2021）",
            "从 1980 年「独生子女」国策，到 2013 年单独二孩、2016 年全面二孩、2021 年三孩——计生政策四十年间四次大调整.",
            "# 中国计划生育政策的历史演变\n\n## 时间轴\n- **1980** 中央 9 月发布《公开信》，正式实施独生子女政策\n" +
            "- **1984** 中央 7 号文件允许农村独女户再生一胎\n- **2013** 十八届三中全会决定启动「单独二孩」\n" +
            "- **2016** 全面放开二孩，《人口与计划生育法》修订\n- **2021** 三孩政策实施，社会抚养费取消\n\n" +
            "## 关键转折\n- 2010 年第六次普查总和生育率首次低于 1.5，进入「超低生育率陷阱」警戒线\n" +
            "- 2016 全面二孩当年出生人口反弹至 1786 万，但 2017 年起即逐年回落\n" +
            "- 2022 年首次出现人口负增长（-85 万），2023 年扩大至 -208 万\n\n" +
            "## 历史评价\n学界普遍认为政策放开节奏滞后于人口结构转型 5-10 年，错过了 2000-2010 的黄金调整窗口期.",
            "2024-01-20"),

        // ============ data ============
        new Doc("doc_d01", "data",
            "2024 年全国出生人口数据",
            "国家统计局 2025 年 1 月发布：2024 年出生人口约 902 万，较 2023 年回升 52 万；总和生育率约 1.05，仍处超低水平.",
            "# 2024 年全国出生人口数据\n\n## 关键数字\n- 出生人口：**902 万**（2023 年 850 万）\n" +
            "- 死亡人口：1093 万\n- 自然增长率：**-1.4‰**（连续第三年负增长）\n- 总和生育率（TFR）：**约 1.05**\n" +
            "- 出生性别比：109.0（持续向 105 自然区间收敛）\n\n## 反弹解读\n2024 年小幅回升主因龙年生育偏好叠加 2023 年积压婚育需求释放，并非趋势性反转。" +
            "1-3 季度高频数据显示 2025 年起将再次回落.\n\n## 区域分布\n- 出生人口最多省份：广东（98 万）、河南（69 万）、山东（68 万）\n" +
            "- 增速最快：广东（+12%）、海南（+9%）—— 与流动人口集聚相关\n- 降幅最大：黑龙江（-15%）、吉林（-13%）",
            "2025-01-17"),

        new Doc("doc_d02", "data",
            "中国总和生育率历年走势（1949-2024）",
            "TFR 从 1963 年峰值 7.5 一路下行：1990 年 2.6，2010 年 1.5，2020 年 1.3，2023 年 1.0，2024 年小幅回升至 1.05.",
            "# 中国总和生育率（TFR）历年走势\n\n## 关键节点\n| 年份 | TFR | 备注 |\n|---|---|---|\n" +
            "| 1949 | 6.1 | 建国初 |\n| 1963 | **7.5** | 历史峰值 |\n| 1980 | 2.3 | 独生子女政策起点 |\n" +
            "| 1990 | 2.6 | 末次接近更替水平 |\n| 2000 | 1.6 | 跌入「低生育率」区间 |\n| 2010 | 1.5 | 六普 |\n" +
            "| 2020 | 1.3 | 七普 |\n| 2023 | **1.0** | 历史最低 |\n| 2024 | 1.05 | 龙年小反弹 |\n\n" +
            "## 国际对比\n2023 年中国 TFR 已低于日本（1.20）、德国（1.36），与韩国（0.72）同处「超超低」区间.\n\n" +
            "## 拐点判定\n据中国人口与发展研究中心模型，若维持 TFR≈1.0，2050 年总人口将降至约 12.6 亿；2100 年或低于 8 亿.",
            "2024-12-30"),

        new Doc("doc_d03", "data",
            "第七次全国人口普查关键指标（2020）",
            "2020 年 11 月开展，结果于 2021 年 5 月公布：总人口 14.12 亿，60+ 占 18.7%，城镇化率 63.89%，性别比 105.07.",
            "# 第七次全国人口普查关键指标\n\n## 总量\n- 全国人口：**14.1178 亿**\n- 较 2010 年六普增长 7206 万（+5.38%）\n" +
            "- 年均增长 0.53%（六普同期 0.57%）\n\n## 结构\n- 0-14 岁：17.95%\n- 15-59 岁：63.35%\n- **60+：18.70%**\n- **65+：13.50%**\n\n" +
            "## 民族 / 教育 / 城镇化\n- 汉族 91.11%，少数民族 8.89%\n- 大学（大专及以上）每 10 万人 15467 人（六普 8930）\n" +
            "- 城镇化率 63.89%（六普 49.68%）\n\n## 性别比\n- 总人口性别比：105.07（女=100）\n- 出生人口性别比：111.3（仍偏离自然区间 103-107）",
            "2021-05-11"),

        new Doc("doc_d04", "data",
            "60 岁以上老年人口结构与预测",
            "2023 年末 60 岁以上人口 2.97 亿（21.1%）；预计 2035 年突破 4 亿，2050 年峰值约 4.87 亿、占比 38%.",
            "# 60 岁以上老年人口结构与预测\n\n## 现状（2023 年末）\n- 60+：**2.97 亿**（21.1%）\n- 65+：**2.17 亿**（15.4%）\n" +
            "- 80+ 高龄：3580 万\n\n## 预测\n据全国老龄办与中国人口与发展研究中心：\n- 2027 年：60+ 突破 3 亿\n" +
            "- 2035 年：60+ 突破 4 亿，老龄化率约 30%（重度老龄化）\n- 2050 年：60+ 峰值 4.87 亿，占比 38.6%\n- 2057 年后老年人口绝对数开始回落\n\n" +
            "## 区域差异\n- 老龄化最深：辽宁（25.7%）、上海（25.0%）、重庆（24.7%）\n" +
            "- 较年轻：西藏（8.7%）、广东（13.0%）、新疆（13.5%）—— 流动人口净流入与少数民族生育率较高\n\n" +
            "## 失能 / 半失能\n2023 年失能与半失能老人约 4500 万，预计 2030 年达 7700 万.",
            "2024-09-25"),

        new Doc("doc_d05", "data",
            "出生人口性别比与婚配市场",
            "出生性别比从 2008 年峰值 121 降至 2024 年 109；20-39 岁适婚男性比同龄女性多约 1700 万，「婚配赤字」压力突出.",
            "# 出生人口性别比与婚配市场\n\n## 出生性别比走势\n| 年份 | 出生性别比 |\n|---|---|\n" +
            "| 1982 | 108.5 |\n| 2000 | 116.9 |\n| 2008 | **121.2**（峰值）|\n| 2015 | 113.5 |\n" +
            "| 2020 | 111.3（七普）|\n| 2024 | 109.0 |\n\n## 婚配赤字\n2024 年 20-39 岁人口中，男性约 2.10 亿、女性约 1.93 亿，差额约 1700 万。" +
            "若按当前生育队列推算，2030 年差额将扩大至 2200 万.\n\n## 影响\n- 农村大龄未婚男性（俗称「光棍」）问题集中在中西部贫困地区\n" +
            "- 跨境婚姻、彩礼通胀、性犯罪风险等社会层面外部性显著\n- 与生育率下降形成「双低」陷阱：适婚男性多但育龄女性少，结婚率持续下降",
            "2024-06-18"),

        // ============ risk ============
        new Doc("doc_r01", "risk",
            "老龄化与养老金可持续性压力",
            "城镇职工基本养老保险抚养比从 2012 年 3.1:1 降至 2023 年 2.4:1；预计 2035 年累计结余将耗尽，需财政或制度改革兜底.",
            "# 老龄化与养老金可持续性压力\n\n## 抚养比走势\n- 2012：3.1 名缴费职工供养 1 名退休\n- 2023：2.4:1\n- 2035 预测：1.8:1\n\n" +
            "## 累计结余\n截至 2023 年末，城镇职工基本养老保险基金累计结余 5.9 万亿元。中国社科院世界社保研究中心 2019 年精算预测：" +
            "若不采取改革措施，**2035 年累计结余将耗尽**.\n\n## 已采取 / 拟议措施\n- 中央调剂金制度（2018 起）：富省补贫省，比例已升至 4.5%\n" +
            "- 国资划转充实社保基金：央企 10% 划转已基本完成\n- 个人养老金第三支柱（2022 起）：账户制 + 税收优惠，覆盖面尚待扩大\n" +
            "- **延迟退休**（2024 年 9 月人大常委会通过）：2025 起男性逐步延至 63、女性 55/58\n\n## 国际经验\n日本于 1985 年抚养比降至 6.0 即启动改革；中国 2024 年起步明显滞后.",
            "2024-04-22"),

        new Doc("doc_r02", "risk",
            "出生人口断崖式下降的连锁效应",
            "2016 年出生 1786 万，2023 年 850 万，七年间腰斩。教育、儿科、母婴消费、产业链依次承压；2025 年小学入学人口将开始大幅下滑.",
            "# 出生人口断崖式下降的连锁效应\n\n## 出生人口走势\n2016 年全面二孩政策落地后：\n- 2016：1786 万（短期反弹峰值）\n" +
            "- 2018：1523 万\n- 2020：1200 万\n- 2022：956 万（首次跌破千万）\n- 2023：850 万（历史最低）\n- 2024：902 万（小幅回升）\n\n" +
            "## 连锁效应（按时间顺序）\n1. **0-3 岁母婴消费**：2017-2022 奶粉行业销售额下滑 15%，纸尿裤腰斩\n" +
            "2. **学前教育**：2022 起多地幼儿园出现关停潮，民办园承压尤甚\n3. **小学入学**：2025 起一年级新生将逐年下降，2030 年较 2024 减少约 30%\n" +
            "4. **儿科**：医院儿科门诊量自 2020 年起下行，三甲医院儿科萎缩\n5. **大学**：约 2035 年起高校生源压力显现，部分省份「双非」院校面临合并\n" +
            "6. **劳动力**：约 2040 年起 18-22 岁劳动新增供给见底，加剧老龄化与抚养比矛盾\n\n## 关键判断\n出生数对宏观的影响有 5-25 年的滞后期；当前看不到的，未来都会到达.",
            "2024-02-28"),

        new Doc("doc_r03", "risk",
            "劳动力短缺与产业链转移风险",
            "15-59 岁劳动年龄人口自 2014 年达峰 9.40 亿后逐年下降，2023 年 8.65 亿；制造业「招工难」持续，部分订单向东南亚转移.",
            "# 劳动力短缺与产业链转移风险\n\n## 劳动年龄人口走势\n- 2014：9.40 亿（峰值）\n- 2020：8.94 亿\n- 2023：8.65 亿\n- 2035 预测：约 8.0 亿\n- 2050 预测：约 6.5 亿\n\n" +
            "## 制造业用工缺口\n人社部 2023 年第四季度「100 个最缺工职业」中，制造业占 41 个。其中机械装配工、焊工、车工长期上榜.\n\n" +
            "## 产业链转移\n- **越南**：电子代工承接，2018-2023 年劳动密集型出口翻倍\n- **印度**：iPhone 代工占比从 0% 升至约 14%（2024）\n" +
            "- **墨西哥**：汽车零部件、家电对美近岸外包\n- 中国应对：自动化（2023 年工业机器人销量全球占比 51%）+ 中西部承接转移\n\n" +
            "## 关键风险\n- 老龄化与高薪并行 → 单位劳动力成本仍上升\n- 适龄劳动力下降 + 高校毕业生结构性错配 → 「技工荒」与「白领过剩」并存",
            "2024-07-12"),

        new Doc("doc_r04", "risk",
            "人口红利消失对宏观经济的影响",
            "总抚养比（少儿+老年）于 2010 年触底 34%后持续上升，2023 年达 47%，预计 2050 年突破 80%；对潜在 GDP 增速、储蓄率、财政产生深远影响.",
            "# 人口红利消失对宏观经济的影响\n\n## 总抚养比走势\n总抚养比 = (0-14 岁 + 65+) / (15-64 岁) × 100%\n- 1982：62%（人口结构未优化）\n" +
            "- 2010：34%（历史最低，「人口红利」窗口期顶峰）\n- 2023：47%\n- 2035 预测：65%\n- 2050 预测：82%\n\n" +
            "## 对宏观三大变量的影响\n### 1. 潜在增长率\n劳动投入贡献由正转负；据社科院测算，人口因素拖累潜在增速年均 0.5-1.0 个百分点（2025-2035）。\n" +
            "### 2. 储蓄率\n生命周期理论预测：老年人净支出 → 总储蓄率下降。中国国民储蓄率从 2010 年 51% 降至 2023 年 44%，预计 2035 年 35%。\n" +
            "### 3. 财政收支\n养老 / 医保支出年均增速 8-10%，远超财政收入增速。基本养老 + 医保占财政支出比重从 2010 年 12% 升至 2023 年 21%.\n\n" +
            "## 政策含义\n- 货币政策：长期中性利率下行\n- 产业政策：从规模驱动转向全要素生产率（TFP）驱动\n- 财政政策：扩大税基（数字经济、消费税）+ 提高 SOE 利润上缴",
            "2024-11-08"),

        // ============ case ============
        new Doc("doc_c01", "case",
            "上海生育补贴与托育试点",
            "上海自 2024 年起对户籍家庭一孩 2000、二孩 5000、三孩 10000 元/年发放育儿补贴；「宝宝屋」普惠托育覆盖全市街镇.",
            "# 上海生育补贴与托育试点\n\n## 现金补贴（2024 年新政）\n- 一孩：2000 元/年\n- 二孩：5000 元/年\n- 三孩：10000 元/年\n" +
            "- 发放至子女 3 周岁；适用对象限上海户籍\n\n## 普惠托育「宝宝屋」\n2023 年起上海在每个街镇至少设 1 个社区「宝宝屋」，提供按时计费的临时托管。" +
            "截至 2024 年末已建成 215 个，全市 0-3 岁托位数达 8.6 万，每千人口 4.1 个.\n\n## 配套措施\n- 公办幼儿园 2-3 岁托班扩容\n" +
            "- 用人单位提供托育服务的，可申请运营补贴\n- 个税育儿专项扣除按 2000 元/月顶格执行\n\n## 效果初评\n2024 年上海户籍出生人口约 7.2 万，较 2023 年增长约 14%（高于全国均值 6%）。" +
            "但分析认为政策效应混合龙年因素，需 2-3 年观察.",
            "2024-09-30"),

        new Doc("doc_c02", "case",
            "山东人口结构调整改革",
            "山东作为人口大省，2023 年起在济南、青岛等市试点二孩三孩家庭购房契税减免，新建住宅配建社区托育空间，鼓励三代同堂购房.",
            "# 山东人口结构调整改革\n\n## 人口背景\n2023 年山东户籍人口 1.02 亿，居全国第二；老龄化率 23.6%（全国 21.1%），承压明显。" +
            "2023 年出生人口 64 万，跌破 1995 年水平.\n\n## 试点措施\n### 购房支持\n- 二孩家庭购房契税地方留存部分全额返还\n- 三孩家庭叠加购房补贴 1-3 万元\n" +
            "- 三代同堂购房（祖父母 + 父母 + 子女同户）住房公积金贷款额度上浮 30%\n\n### 托育与教育\n- 新建住宅小区按每千人口 4 个托位标准配建\n" +
            "- 公办幼儿园 2-3 岁托班全面铺开（济南、青岛 2024 年 100% 街道覆盖）\n\n### 灵活就业\n- 育龄女性灵活就业群体单独缴存生育保险，财政补 50%\n\n## 初步成效\n2024 年山东出生人口回升至 68 万，济南、青岛回升幅度高于省平均。" +
            "但学界提醒地方刺激可能造成人口在省内东西部之间的搬运而非净增.",
            "2024-05-08"),

        new Doc("doc_c03", "case",
            "四川生育登记改革（取消婚姻状态限制）",
            "四川 2023 年 1 月起取消生育登记的结婚证要求，将生育登记从户籍管理工具回归公共服务属性，引发全国关注.",
            "# 四川生育登记改革\n\n## 改革要点\n2023 年 1 月 30 日起：\n- 生育登记取消结婚证要求\n- 取消生育数量限制\n" +
            "- 简化程序：在常住地（不限户籍地）即可办理\n\n## 政策意图\n- 让生育登记回归「统计 + 公共服务衔接」功能\n" +
            "- 保障非婚生育子女平等享有产假、生育保险、公共医疗权益\n- 与三孩政策「取消社会抚养费」精神一致\n\n## 社会反响\n- 支持方：保护未婚妈妈与非婚生子女权益，是观念进步\n" +
            "- 担忧方：是否变相鼓励非婚生育、对传统家庭结构的冲击\n- 学界主流意见：在生育率持续低迷背景下，登记制度的婚姻门槛对实际生育意愿影响有限，但对女性权益保护意义重大\n\n" +
            "## 后续跟进\n2023 年广东、北京等地陆续放开类似限制；2024 年国家卫健委发文鼓励各地「探索建立」非婚生育登记便利化机制.",
            "2023-02-15")
    );

    // ============================================================
    // helpers
    // ============================================================

    private static class Doc {
        final String docId;
        final String scope;
        final String title;
        final String snippet;
        final String content;
        final String updatedAt;

        Doc(String docId, String scope, String title, String snippet, String content, String updatedAt) {
            this.docId = docId;
            this.scope = scope;
            this.title = title;
            this.snippet = snippet;
            this.content = content;
            this.updatedAt = updatedAt;
        }
    }

    private static Map<String, Object> errorBody(String code, String message) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("code", code);
        m.put("message", message);
        return m;
    }

    private static void writeJson(HttpServletResponse resp, int status, Map<String, Object> body) throws IOException {
        resp.setStatus(status);
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json; charset=UTF-8");
        resp.setHeader("Cache-Control", "no-store");
        // 简易 CORS：mock 期跨端调试时常需要
        resp.setHeader("Access-Control-Allow-Origin", "*");
        resp.getWriter().write(JsonUtil.toJson(body == null ? Collections.emptyMap() : body));
    }

    private static Map<String, Object> readBody(HttpServletRequest req) {
        String ct = req.getContentType();
        if (ct == null || !ct.toLowerCase().contains("application/json")) {
            return Collections.emptyMap();
        }
        StringBuilder sb = new StringBuilder();
        try (BufferedReader br = req.getReader()) {
            String line;
            while ((line = br.readLine()) != null) sb.append(line);
        } catch (IOException e) {
            return Collections.emptyMap();
        }
        String raw = sb.toString();
        if (raw.isEmpty()) return Collections.emptyMap();
        try {
            return JsonUtil.fromJsonToMap(raw);
        } catch (Throwable t) {
            return Collections.emptyMap();
        }
    }

    private static String asStr(Object o) { return o == null ? null : String.valueOf(o); }

    private static int asInt(Object o, int def) {
        if (o == null) return def;
        if (o instanceof Number) return ((Number) o).intValue();
        try { return Integer.parseInt(String.valueOf(o)); } catch (Exception e) { return def; }
    }

    @SuppressWarnings("unchecked")
    private static List<String> asStringList(Object o) {
        if (o == null) return null;
        if (o instanceof List) {
            List<String> out = new ArrayList<>();
            for (Object x : (List<Object>) o) {
                if (x == null) continue;
                String s = String.valueOf(x).trim();
                if (!s.isEmpty()) out.add(s);
            }
            return out.isEmpty() ? null : out;
        }
        if (o instanceof String) {
            // 兼容 CSV 风格
            String s = ((String) o).trim();
            if (s.isEmpty()) return null;
            List<String> out = new ArrayList<>();
            for (String tok : s.split(",")) {
                String t = tok.trim();
                if (!t.isEmpty()) out.add(t);
            }
            return out.isEmpty() ? null : out;
        }
        return null;
    }

    private static int clamp(int v, int min, int max) { return Math.max(min, Math.min(max, v)); }
    private static double round2(double d) { return Math.round(d * 100.0) / 100.0; }
    private static String safeMsg(Throwable t) {
        if (t == null) return "unknown";
        String m = t.getMessage();
        return m == null ? t.getClass().getSimpleName() : m;
    }
}
