#include "pq_app.h"
#include "tinyxml2.h"

#include <windows.h>

#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>

/*
 * icd_mapper.cpp
 *
 * 功能：把外部 ICD/XML 中 LN/DOI 的 desc 描述补写到工作簿
 *       “未匹配的原始数据”工作表的“icd中代表的字段”列。
 *
 * 输入：
 *   icdPath      外部 ICD/XML 文件，可能是 UTF-8，也可能声明/实际使用 GBK/GB2312。
 *   workbookPath main.cpp 生成的 Excel 2003 XML 格式 .xls。
 *
 * 输出：
 *   直接修改 workbookPath。message 返回解析结果、匹配数量或错误原因。
 *
 * 核心匹配规则：
 *   1. 从 ICD 的每个 LN 节点读取 lnClass + inst，并遍历其 DOI 子节点；
 *   2. 组成 DO key：lnClass + inst + "$MX$" + DOI/@name；
 *   3. 工作簿未匹配页的“原始路径”如果等于该 key，或以 key + "$" 开头，
 *      就认为该原始 MMS 路径属于这个 DOI，把 DOI/@desc 写入目标列。
 *
 * 阅读本文件时可留意的 C++/库语法：
 *   - namespace fs = std::filesystem 是命名空间别名，后文 fs::path 即文件系统路径类型；
 *   - namespace { ... } 是匿名命名空间，使辅助函数只在当前编译单元内可见；
 *   - tinyxml2::XMLDocument 拥有解析后的节点，XMLNode/XMLElement 指针不能脱离文档存活；
 *   - std::unordered_map 用散列表保存 ICD key -> desc，适合大量精确查询；
 *   - std::sort/std::transform 中的 [](...) { ... } 是短生命周期 lambda（匿名函数）；
 *   - MultiByteToWideChar/WideCharToMultiByte 负责 Windows 本地代码页与 UTF-8 转换。
 */

namespace fs = std::filesystem;

namespace {
/**
 * Function: readAllBinary
 *
 * @brief 读取或加载 “readAllBinary” 对应的数据，并转换为本模块后续逻辑使用的内存结构。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param path 文件或目录路径；函数按 UTF-8/Windows 路径规则读取或写入。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
std::string readAllBinary(const fs::path& path) {
    std::ifstream ifs(path, std::ios::binary);
    if (!ifs) throw std::runtime_error("Failed to open file: " + path.string());
    std::ostringstream ss;
    ss << ifs.rdbuf();
    return ss.str();
}
/**
 * Function: writeAllBinary
 *
 * @brief 把 “writeAllBinary” 对应的内存数据序列化并写入目标位置。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param path 文件或目录路径；函数按 UTF-8/Windows 路径规则读取或写入。
 * @param text 待解析、比较、显示或发送的 UTF-8 文本。
 */
void writeAllBinary(const fs::path& path, const std::string& text) {
    std::ofstream ofs(path, std::ios::binary);
    if (!ofs) throw std::runtime_error("Failed to write file: " + path.string());
    ofs << text;
}
/**
 * Function: lowerAscii
 *
 * @brief 规范化 “lowerAscii” 对应的字符串或标识，减少格式差异对匹配造成的影响。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param s 调用方传入的业务参数；其用途与约束由类型、名称及函数体共同定义。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
std::string lowerAscii(std::string s) {
    std::transform(s.begin(), s.end(), s.begin(),
                   [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
    return s;
}
/**
 * Function: declaresGbEncoding
 *
 * @brief 判断 “declaresGbEncoding” 所表达的条件；该函数只读取输入，不负责修改业务状态。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param text 待解析、比较、显示或发送的 UTF-8 文本。
 * @return 条件成立或操作成功时为 true，否则为 false。
 */
bool declaresGbEncoding(const std::string& text) {
    std::string head = lowerAscii(text.substr(0, std::min<size_t>(text.size(), 256)));
    return head.find("gb2312") != std::string::npos ||
           head.find("gbk") != std::string::npos ||
           head.find("gb18030") != std::string::npos;
}
/**
 * Function: isLikelyUtf8
 *
 * @brief 判断 “isLikelyUtf8” 所表达的条件；该函数只读取输入，不负责修改业务状态。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param text 待解析、比较、显示或发送的 UTF-8 文本。
 * @return 条件成立或操作成功时为 true，否则为 false。
 */
bool isLikelyUtf8(const std::string& text) {
    // 只做 UTF-8 字节合法性判断，不判断语义；合法就优先按 UTF-8 给 tinyxml2。
    int remaining = 0;
    for (unsigned char c : text) {
        if (remaining == 0) {
            if ((c & 0x80) == 0) continue;
            if ((c & 0xE0) == 0xC0) remaining = 1;
            else if ((c & 0xF0) == 0xE0) remaining = 2;
            else if ((c & 0xF8) == 0xF0) remaining = 3;
            else return false;
        } else {
            if ((c & 0xC0) != 0x80) return false;
            --remaining;
        }
    }
    return remaining == 0;
}
/**
 * Function: ansiToUtf8
 *
 * @brief 实现 “ansiToUtf8” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param text 待解析、比较、显示或发送的 UTF-8 文本。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
std::string ansiToUtf8(const std::string& text) {
    // 936 是 Windows 简体中文代码页，兼容常见 GBK/GB2312 ICD 文件。
    if (text.empty()) return "";
    int wideLen = MultiByteToWideChar(936, 0, text.data(), (int)text.size(), nullptr, 0);
    if (wideLen <= 0) return text;
    std::wstring wide(wideLen, L'\0');
    MultiByteToWideChar(936, 0, text.data(), (int)text.size(), wide.data(), wideLen);
    int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wide.data(), wideLen, nullptr, 0, nullptr, nullptr);
    std::string out(utf8Len, '\0');
    WideCharToMultiByte(CP_UTF8, 0, wide.data(), wideLen, out.data(), utf8Len, nullptr, nullptr);
    return out;
}
/**
 * Function: loadXmlText
 *
 * @brief 读取或加载 “loadXmlText” 对应的数据，并转换为本模块后续逻辑使用的内存结构。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param path 文件或目录路径；函数按 UTF-8/Windows 路径规则读取或写入。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
std::string loadXmlText(const fs::path& path) {
    std::string raw = readAllBinary(path);
    // ICD 声明 GB 编码或字节本身不像 UTF-8 时，先转成 UTF-8 再交给 tinyxml2。
    if (declaresGbEncoding(raw) || !isLikelyUtf8(raw)) return ansiToUtf8(raw);
    return raw;
}
/**
 * Function: localName
 *
 * @brief 实现 “localName” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param name 待解析、比较、显示或发送的 UTF-8 文本。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
std::string localName(const char* name) {
    if (!name) return "";
    std::string s(name);
    size_t pos = s.find(':');
    return pos == std::string::npos ? s : s.substr(pos + 1);
}
/**
 * Function: attr
 *
 * @brief 实现 “attr” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param e 调用方传入的业务参数；其用途与约束由类型、名称及函数体共同定义。
 * @param name 待解析、比较、显示或发送的 UTF-8 文本。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
std::string attr(const tinyxml2::XMLElement* e, const char* name) {
    const char* v = e ? e->Attribute(name) : nullptr;
    return v ? v : "";
}
/**
 * Function: collectIcdDescriptions
 *
 * @brief 遍历输入结构并收集 “collectIcdDescriptions” 所需的候选数据。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param element 当前处理的数据行、单元格、XML 节点或模型项。
 * @param descByDo 调用方传入的业务参数；其用途与约束由类型、名称及函数体共同定义。
 */
void collectIcdDescriptions(const tinyxml2::XMLElement* element,
                            std::unordered_map<std::string, std::string>& descByDo) {
    if (!element) return;
    if (localName(element->Name()) == "LN") {
        std::string lnClass = attr(element, "lnClass");
        std::string inst = attr(element, "inst");
        if (!lnClass.empty()) {
            for (const tinyxml2::XMLElement* child = element->FirstChildElement();
                 child;
                 child = child->NextSiblingElement()) {
                if (localName(child->Name()) != "DOI") continue;
                std::string doiName = attr(child, "name");
                std::string desc = attr(child, "desc");
                if (doiName.empty() || desc.empty()) continue;
                // 这里构造的是 origin 原始路径前缀。后面路径可继续带 $mag$f 等 DA 后缀。
                std::string key = lnClass + inst + "$MX$" + doiName;
                descByDo[key] = desc;
            }
        }
    }
    for (const tinyxml2::XMLElement* child = element->FirstChildElement();
         child;
         child = child->NextSiblingElement()) {
        collectIcdDescriptions(child, descByDo);
    }
}
/**
 * Function: parseIcdDescriptions
 *
 * @brief 解析 “parseIcdDescriptions” 对应的文本或结构，提取经过校验的业务字段。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param icdPath 文件或目录路径；函数按 UTF-8/Windows 路径规则读取或写入。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
std::unordered_map<std::string, std::string> parseIcdDescriptions(const fs::path& icdPath) {
    tinyxml2::XMLDocument doc;
    std::string xml = loadXmlText(icdPath);
    tinyxml2::XMLError rc = doc.Parse(xml.c_str(), xml.size());
    if (rc != tinyxml2::XML_SUCCESS) {
        throw std::runtime_error("Failed to parse ICD XML: " + std::to_string((int)rc));
    }
    std::unordered_map<std::string, std::string> descByDo;
    collectIcdDescriptions(doc.RootElement(), descByDo);
    return descByDo;
}
/**
 * Function: cellText
 *
 * @brief 实现 “cellText” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param cell 当前处理的数据行、单元格、XML 节点或模型项。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
std::string cellText(const tinyxml2::XMLElement* cell) {
    const tinyxml2::XMLElement* data = cell ? cell->FirstChildElement("Data") : nullptr;
    const char* text = data ? data->GetText() : nullptr;
    return text ? text : "";
}
/**
 * Function: cellIndex
 *
 * @brief 实现 “cellIndex” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param cell 当前处理的数据行、单元格、XML 节点或模型项。
 * @param fallback 调用方传入的业务参数；其用途与约束由类型、名称及函数体共同定义。
 * @return 整数状态、索引或计数；入口函数以 0 表示成功。
 */
int cellIndex(const tinyxml2::XMLElement* cell, int fallback) {
    const char* idx = cell ? cell->Attribute("ss:Index") : nullptr;
    return idx && *idx ? std::max(1, std::atoi(idx)) : fallback;
}
/**
 * Function: cellAt
 *
 * @brief 实现 “cellAt” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param row 当前处理的数据行、单元格、XML 节点或模型项。
 * @param col 索引、数量、范围或偏移参数；具体边界由函数体校验。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
tinyxml2::XMLElement* cellAt(tinyxml2::XMLElement* row, int col) {
    int current = 1;
    for (tinyxml2::XMLElement* cell = row ? row->FirstChildElement("Cell") : nullptr;
         cell;
         cell = cell->NextSiblingElement("Cell")) {
        current = cellIndex(cell, current);
        if (current == col) return cell;
        ++current;
    }
    return nullptr;
}
/**
 * Function: ensureCell
 *
 * @brief 向目标结构追加或保证存在 “ensureCell” 所描述的数据。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param doc tinyxml2 文档对象；节点的生命周期由该文档管理。
 * @param row 当前处理的数据行、单元格、XML 节点或模型项。
 * @param col 索引、数量、范围或偏移参数；具体边界由函数体校验。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
tinyxml2::XMLElement* ensureCell(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* row, int col) {
    if (tinyxml2::XMLElement* existing = cellAt(row, col)) return existing;
    // Excel XML 允许通过 ss:Index 跳列；补列时直接设置 ss:Index，避免改动前面已有单元格。
    tinyxml2::XMLElement* cell = doc.NewElement("Cell");
    cell->SetAttribute("ss:Index", col);
    row->InsertEndChild(cell);
    return cell;
}
/**
 * Function: setCellText
 *
 * @brief 把 “setCellText” 对应的变更应用到模型、控件或输出文档。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param doc tinyxml2 文档对象；节点的生命周期由该文档管理。
 * @param cell 当前处理的数据行、单元格、XML 节点或模型项。
 * @param text 待解析、比较、显示或发送的 UTF-8 文本。
 */
void setCellText(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* cell, const std::string& text) {
    tinyxml2::XMLElement* data = cell->FirstChildElement("Data");
    if (!data) {
        data = doc.NewElement("Data");
        data->SetAttribute("ss:Type", "String");
        cell->InsertEndChild(data);
    }
    data->SetText(text.c_str());
}
/**
 * Function: sortedKeysByLength
 *
 * @brief 实现 “sortedKeysByLength” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param values 调用方传入的业务参数；其用途与约束由类型、名称及函数体共同定义。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
std::vector<std::string> sortedKeysByLength(const std::unordered_map<std::string, std::string>& values) {
    std::vector<std::string> keys;
    keys.reserve(values.size());
    for (const auto& kv : values) keys.push_back(kv.first);
    // 长路径优先，避免短 key 先命中长 key 的前缀。
    std::sort(keys.begin(), keys.end(), [](const std::string& a, const std::string& b) {
        if (a.size() != b.size()) return a.size() > b.size();
        return a < b;
    });
    return keys;
}
/**
 * Function: pathMatchesDo
 *
 * @brief 实现 “pathMatchesDo” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param rawPath 文件或目录路径；函数按 UTF-8/Windows 路径规则读取或写入。
 * @param doKey 调用方传入的业务参数；其用途与约束由类型、名称及函数体共同定义。
 * @return 条件成立或操作成功时为 true，否则为 false。
 */
bool pathMatchesDo(const std::string& rawPath, const std::string& doKey) {
    // rawPath 可以正好是 DO，也可以是 DO$DA...；必须在 "$" 边界匹配，避免误命中相似前缀。
    return rawPath == doKey ||
           (rawPath.size() > doKey.size() &&
            rawPath.compare(0, doKey.size(), doKey) == 0 &&
            rawPath[doKey.size()] == '$');
}
/**
 * Function: descForPath
 *
 * @brief 实现 “descForPath” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param rawPath 文件或目录路径；函数按 UTF-8/Windows 路径规则读取或写入。
 * @param descByDo 调用方传入的业务参数；其用途与约束由类型、名称及函数体共同定义。
 * @param orderedKeys 调用方传入的业务参数；其用途与约束由类型、名称及函数体共同定义。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
std::string descForPath(const std::string& rawPath,
                        const std::unordered_map<std::string, std::string>& descByDo,
                        const std::vector<std::string>& orderedKeys) {
    for (const auto& key : orderedKeys) {
        if (pathMatchesDo(rawPath, key)) {
            auto it = descByDo.find(key);
            return it == descByDo.end() ? "" : it->second;
        }
    }
    return "";
}
/**
 * Function: findWorksheet
 *
 * @brief 在现有数据结构中查找 “findWorksheet” 对应的对象或位置。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param doc tinyxml2 文档对象；节点的生命周期由该文档管理。
 * @param sheetName 调用方传入的业务参数；其用途与约束由类型、名称及函数体共同定义。
 * @return 构造、解析或查找到的结果；所有权与生命周期遵循返回类型。
 */
tinyxml2::XMLElement* findWorksheet(tinyxml2::XMLDocument& doc, const char* sheetName) {
    tinyxml2::XMLElement* workbook = doc.FirstChildElement("Workbook");
    for (tinyxml2::XMLElement* ws = workbook ? workbook->FirstChildElement("Worksheet") : nullptr;
         ws;
         ws = ws->NextSiblingElement("Worksheet")) {
        const char* name = ws->Attribute("ss:Name");
        if (name && std::string(name) == sheetName) return ws;
    }
    return nullptr;
}
/**
 * Function: headerColumn
 *
 * @brief 实现 “headerColumn” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param headerRow 当前处理的数据行、单元格、XML 节点或模型项。
 * @param title 待解析、比较、显示或发送的 UTF-8 文本。
 * @return 整数状态、索引或计数；入口函数以 0 表示成功。
 */
int headerColumn(tinyxml2::XMLElement* headerRow, const std::string& title) {
    int current = 1;
    for (tinyxml2::XMLElement* cell = headerRow ? headerRow->FirstChildElement("Cell") : nullptr;
         cell;
         cell = cell->NextSiblingElement("Cell")) {
        current = cellIndex(cell, current);
        if (cellText(cell) == title) return current;
        ++current;
    }
    return 0;
}
} // namespace
/**
 * Function: pqApplyIcdDescriptionsToWorkbook
 *
 * @brief 实现 “pqApplyIcdDescriptionsToWorkbook” 对应的模块内功能。
 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML，并使用 Windows 代码页 API 兼容 GBK/GB2312；文件操作采用 std::filesystem 与二进制流，失败时抛出异常交给公共入口处理。
 * @param icdPath 文件或目录路径；函数按 UTF-8/Windows 路径规则读取或写入。
 * @param workbookPath 文件或目录路径；函数按 UTF-8/Windows 路径规则读取或写入。
 * @param message 输出参数；函数在成功或失败时写入结果或诊断信息。
 * @return 条件成立或操作成功时为 true，否则为 false。
 */
bool pqApplyIcdDescriptionsToWorkbook(const std::string& icdPath,
                                      const std::string& workbookPath,
                                      std::string* message) {
    try {
        // 第一步：解析 ICD，得到 “DO 路径前缀 -> 中文描述”。
        auto descByDo = parseIcdDescriptions(fs::u8path(icdPath));
        if (descByDo.empty()) {
            if (message) *message = "ICD 中没有解析到 LN/DOI desc";
            return false;
        }
        std::vector<std::string> orderedKeys = sortedKeysByLength(descByDo);

        // 第二步：读取 main.cpp 输出的工作簿。该 .xls 实际是 XML，可直接用 tinyxml2 修改。
        tinyxml2::XMLDocument doc;
        std::string workbookXml = readAllBinary(fs::u8path(workbookPath));
        tinyxml2::XMLError rc = doc.Parse(workbookXml.c_str(), workbookXml.size());
        if (rc != tinyxml2::XML_SUCCESS) {
            if (message) *message = "表格文件解析失败: " + std::to_string((int)rc);
            return false;
        }

        // 第三步：只处理“未匹配的原始数据”页，因为已匹配页已经有明确映射关系。
        tinyxml2::XMLElement* sheet = findWorksheet(doc, "未匹配的原始数据");
        if (!sheet) {
            if (message) *message = "没有找到 sheet: 未匹配的原始数据";
            return false;
        }
        tinyxml2::XMLElement* table = sheet->FirstChildElement("Table");
        tinyxml2::XMLElement* header = table ? table->FirstChildElement("Row") : nullptr;
        int pathCol = headerColumn(header, "原始路径");
        int icdCol = headerColumn(header, "icd中代表的字段");
        if (pathCol <= 0 || icdCol <= 0) {
            if (message) *message = "未匹配 sheet 缺少 原始路径 或 icd中代表的字段 列";
            return false;
        }

        // 第四步：逐行读取原始路径，按 DO 前缀回填 DOI 描述。
        int matched = 0;
        for (tinyxml2::XMLElement* row = header ? header->NextSiblingElement("Row") : nullptr;
             row;
             row = row->NextSiblingElement("Row")) {
            tinyxml2::XMLElement* pathCell = cellAt(row, pathCol);
            std::string rawPath = cellText(pathCell);
            if (rawPath.empty()) continue;
            std::string desc = descForPath(rawPath, descByDo, orderedKeys);
            if (desc.empty()) continue;
            tinyxml2::XMLElement* icdCell = ensureCell(doc, row, icdCol);
            setCellText(doc, icdCell, desc);
            ++matched;
        }

        tinyxml2::XMLPrinter printer;
        doc.Print(&printer);
        writeAllBinary(fs::u8path(workbookPath), printer.CStr());
        if (message) {
            *message = "ICD 导入完成，DOI=" + std::to_string(descByDo.size()) +
                       "，填入未匹配原始数据=" + std::to_string(matched);
        }
        return true;
    } catch (const std::exception& e) {
        if (message) *message = e.what();
        return false;
    }
}
