#define NOMINMAX
#ifndef UNICODE
#define UNICODE
#endif
#ifndef _UNICODE
#define _UNICODE
#endif

#include "pq_app.h"

#include "json.hpp"

#include <windows.h>
#include <commctrl.h>
#include <commdlg.h>

#include <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <map>
#include <memory>
#include <sstream>

namespace {

using json = nlohmann::json;
namespace fs = std::filesystem;

enum ControlId {
    IDC_TAB = 900,
    IDC_PAGE_CONFIG,
    IDC_PAGE_LEDGER,
    IDC_PAGE_JSON,
    IDC_FRONT_INST = 1001,
    IDC_FRONT_IP,
    IDC_TERMINAL_STATUS,
    IDC_MONITOR_STATUS,
    IDC_ICD_FLAG,
    IDC_WEB_DEVICE,
    IDC_WEB_ICD,
    IDC_WEB_FILEDOWNLOAD,
    IDC_XML_SAVE_DIR,
    IDC_PRODUCER_ACCESS_KEY,
    IDC_PRODUCER_SECRET_KEY,
    IDC_CONSUMER_ACCESS_KEY,
    IDC_CONSUMER_SECRET_KEY,
    IDC_PRODUCER,
    IDC_IPPORT,
    IDC_TOPIC_LOG,
    IDC_TAG_LOG,
    IDC_KEY_LOG,
    IDC_CONSUMER,
    IDC_CONSUMER_IPPORT,
    IDC_DATA_STAT_TOPIC,
    IDC_DATA_REALTIME_TOPIC,
    IDC_DATA_STAT_TAG,
    IDC_DATA_STAT_KEY,
    IDC_DATA_REALTIME_TAG,
    IDC_DATA_REALTIME_KEY,
    IDC_TRACE_TOPIC,
    IDC_TRACE_TAG,
    IDC_TRACE_KEY,
    IDC_XML_PATH,
    IDC_JSON_PATH,
    IDC_ORIGIN_PATHS,
    IDC_OUTPUT_PATH,
    IDC_FRONT_TYPE,
    IDC_TREE,
    IDC_DETAIL,
    IDC_LOG,
    IDC_JSON_TREE,
    IDC_JSON_DETAIL,
    IDC_GET_LEDGER = 2001,
    IDC_INIT_MQ,
    IDC_TRACE,
    IDC_PARSE,
    IDC_BROWSE_XML,
    IDC_BROWSE_JSON,
    IDC_ADD_ORIGIN
};

struct NodeRef {
    int device = -1;
    int monitor = -1;
};

struct JsonItem {
    std::string kind;
    std::string title;
    std::string body;
    std::string path;
    std::string dataType;
};

struct JsonNodeRef {
    int index = -1;
};

struct AppState {
    HINSTANCE inst = nullptr;
    HWND hwnd = nullptr;
    HWND tab = nullptr;
    HWND pageConfig = nullptr;
    HWND pageLedger = nullptr;
    HWND pageJson = nullptr;
    HWND tree = nullptr;
    HWND detail = nullptr;
    HWND log = nullptr;
    HWND jsonTree = nullptr;
    HWND jsonDetail = nullptr;
    HWND frontType = nullptr;
    HFONT font = nullptr;
    bool ownsFont = false;
    std::vector<PqLedgerDevice> devices;
    std::vector<std::unique_ptr<NodeRef>> refs;
    std::vector<JsonItem> jsonItems;
    std::vector<std::unique_ptr<JsonNodeRef>> jsonRefs;
    std::string activeTraceDir;
    int selectedDevice = -1;
    int selectedMonitor = -1;
    int selectedJson = -1;
};

AppState* g_app = nullptr;
constexpr UINT WM_PQ_MQ_EVENT = WM_APP + 101;

std::wstring u8w(const std::string& s) {
    if (s.empty()) return L"";
    int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0);
    std::wstring out(n, L'\0');
    MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), out.data(), n);
    return out;
}

std::string w8(const std::wstring& s) {
    if (s.empty()) return "";
    int n = WideCharToMultiByte(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0, nullptr, nullptr);
    std::string out(n, '\0');
    WideCharToMultiByte(CP_UTF8, 0, s.data(), (int)s.size(), out.data(), n, nullptr, nullptr);
    return out;
}

struct FindCtx {
    int id;
    HWND found;
};

BOOL CALLBACK enumFindControl(HWND child, LPARAM lp) {
    auto* ctx = reinterpret_cast<FindCtx*>(lp);
    if (GetDlgCtrlID(child) == ctx->id) {
        ctx->found = child;
        return FALSE;
    }
    return TRUE;
}

HWND findControl(HWND root, int id) {
    HWND direct = GetDlgItem(root, id);
    if (direct) return direct;
    FindCtx ctx{id, nullptr};
    EnumChildWindows(root, enumFindControl, reinterpret_cast<LPARAM>(&ctx));
    return ctx.found;
}

std::string getText(HWND hwnd, int id) {
    HWND h = findControl(hwnd, id);
    if (!h) return "";
    int len = GetWindowTextLengthW(h);
    std::wstring text(len + 1, L'\0');
    GetWindowTextW(h, text.data(), len + 1);
    text.resize(len);
    return w8(text);
}

void setText(HWND hwnd, int id, const std::string& value) {
    HWND h = findControl(hwnd, id);
    if (h) SetWindowTextW(h, u8w(value).c_str());
}

void applyFont(AppState& app, HWND ctl) {
    SendMessageW(ctl, WM_SETFONT, (WPARAM)app.font, TRUE);
}

LRESULT CALLBACK panelProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
    if ((msg == WM_COMMAND || msg == WM_NOTIFY) && g_app && g_app->hwnd) {
        return SendMessageW(g_app->hwnd, msg, wp, lp);
    }
    return DefWindowProcW(hwnd, msg, wp, lp);
}

HWND addLabel(AppState& app, HWND parent, const wchar_t* text, int x, int y, int w, int h) {
    HWND ctl = CreateWindowExW(0, L"STATIC", text, WS_CHILD | WS_VISIBLE | SS_LEFT | SS_NOPREFIX,
                               x, y + 4, w, h, parent, nullptr, app.inst, nullptr);
    applyFont(app, ctl);
    return ctl;
}

HWND addEdit(AppState& app, HWND parent, int id, const std::string& text, int x, int y, int w, int h) {
    h = std::max(h, 28);
    HWND ctl = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", u8w(text).c_str(),
                               WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL | ES_NOHIDESEL,
                               x, y, w, h, parent, (HMENU)(INT_PTR)id, app.inst, nullptr);
    applyFont(app, ctl);
    SendMessageW(ctl, EM_SETMARGINS, EC_LEFTMARGIN | EC_RIGHTMARGIN, MAKELPARAM(5, 5));
    return ctl;
}

HWND addMemo(AppState& app, HWND parent, int id, int x, int y, int w, int h) {
    HWND ctl = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
                               WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_MULTILINE |
                                   ES_AUTOVSCROLL | WS_VSCROLL,
                               x, y, w, h, parent, (HMENU)(INT_PTR)id, app.inst, nullptr);
    applyFont(app, ctl);
    return ctl;
}

HWND addButton(AppState& app, HWND parent, int id, const wchar_t* text, int x, int y, int w, int h) {
    HWND ctl = CreateWindowExW(0, L"BUTTON", text, WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
                               x, y, w, h, parent, (HMENU)(INT_PTR)id, app.inst, nullptr);
    applyFont(app, ctl);
    return ctl;
}

HWND addGroup(AppState& app, HWND parent, const wchar_t* text, int x, int y, int w, int h) {
    HWND ctl = CreateWindowExW(0, L"BUTTON", text, WS_CHILD | WS_VISIBLE | BS_GROUPBOX,
                               x, y, w, h, parent, nullptr, app.inst, nullptr);
    applyFont(app, ctl);
    return ctl;
}

void addLabeledEdit(AppState& app, HWND parent, const wchar_t* label, int id, const std::string& value,
                    int x, int y, int labelW, int editW) {
    addLabel(app, parent, label, x, y, labelW, 22);
    addEdit(app, parent, id, value, x + labelW, y, editW, 28);
}

void appendLogW(AppState& app, const std::wstring& line) {
    if (!app.log) return;
    std::wstring text = line + L"\r\n";
    int len = GetWindowTextLengthW(app.log);
    SendMessageW(app.log, EM_SETSEL, len, len);
    SendMessageW(app.log, EM_REPLACESEL, FALSE, (LPARAM)text.c_str());
}

void appendLog(AppState& app, const std::string& line) {
    appendLogW(app, u8w(line));
}

std::string trimAscii(std::string s) {
    auto isSpace = [](unsigned char c) { return std::isspace(c) != 0; };
    s.erase(s.begin(), std::find_if(s.begin(), s.end(), [&](char c) { return !isSpace(static_cast<unsigned char>(c)); }));
    s.erase(std::find_if(s.rbegin(), s.rend(), [&](char c) { return !isSpace(static_cast<unsigned char>(c)); }).base(), s.end());
    return s;
}

std::vector<std::string> splitPathList(std::string text) {
    for (char& c : text) {
        if (c == ';' || c == '\r' || c == '\n') c = '\n';
    }
    std::vector<std::string> paths;
    std::stringstream ss(text);
    std::string part;
    while (std::getline(ss, part, '\n')) {
        part = trimAscii(part);
        if (!part.empty()) paths.push_back(part);
    }
    return paths;
}

std::string joinPaths(const std::vector<std::string>& paths);

std::string selectedFrontTypeFromWindow(HWND hwnd) {
    HWND combo = findControl(hwnd, IDC_FRONT_TYPE);
    if (!combo) return "cfg_stat_data";
    int idx = (int)SendMessageW(combo, CB_GETCURSEL, 0, 0);
    wchar_t buf[128]{};
    SendMessageW(combo, CB_GETLBTEXT, idx < 0 ? 0 : idx, (LPARAM)buf);
    std::string value = w8(buf);
    return value.empty() ? "cfg_stat_data" : value;
}

PqToolConfig readConfig(HWND hwnd) {
    PqToolConfig cfg = pqDefaultConfig();
    cfg.frontInst = getText(hwnd, IDC_FRONT_INST);
    cfg.frontIp = getText(hwnd, IDC_FRONT_IP);
    cfg.terminalStatus = getText(hwnd, IDC_TERMINAL_STATUS);
    cfg.monitorStatus = getText(hwnd, IDC_MONITOR_STATUS);
    cfg.icdFlag = getText(hwnd, IDC_ICD_FLAG);
    cfg.webDevice = getText(hwnd, IDC_WEB_DEVICE);
    cfg.webIcd = getText(hwnd, IDC_WEB_ICD);
    cfg.webFiledownload = getText(hwnd, IDC_WEB_FILEDOWNLOAD);
    cfg.xmlSaveDir = getText(hwnd, IDC_XML_SAVE_DIR);
    cfg.producer = getText(hwnd, IDC_PRODUCER);
    cfg.ipport = getText(hwnd, IDC_IPPORT);
    cfg.producerAccessKey = getText(hwnd, IDC_PRODUCER_ACCESS_KEY);
    cfg.producerSecretKey = getText(hwnd, IDC_PRODUCER_SECRET_KEY);
    cfg.topicLOG = getText(hwnd, IDC_TOPIC_LOG);
    cfg.tagLOG = getText(hwnd, IDC_TAG_LOG);
    cfg.keyLOG = getText(hwnd, IDC_KEY_LOG);
    cfg.consumer = getText(hwnd, IDC_CONSUMER);
    cfg.consumerIpport = getText(hwnd, IDC_CONSUMER_IPPORT);
    cfg.consumerAccessKey = getText(hwnd, IDC_CONSUMER_ACCESS_KEY);
    cfg.consumerSecretKey = getText(hwnd, IDC_CONSUMER_SECRET_KEY);
    cfg.dataStatTopic = getText(hwnd, IDC_DATA_STAT_TOPIC);
    cfg.dataRealtimeTopic = getText(hwnd, IDC_DATA_REALTIME_TOPIC);
    cfg.dataStatTag = getText(hwnd, IDC_DATA_STAT_TAG);
    cfg.dataStatKey = getText(hwnd, IDC_DATA_STAT_KEY);
    cfg.dataRealtimeTag = getText(hwnd, IDC_DATA_REALTIME_TAG);
    cfg.dataRealtimeKey = getText(hwnd, IDC_DATA_REALTIME_KEY);
    std::string frontType = selectedFrontTypeFromWindow(hwnd);
    if (frontType == "cfg_3s_data") {
        cfg.dataTopic = cfg.dataRealtimeTopic;
        cfg.dataTag = cfg.dataRealtimeTag;
        cfg.dataKey = cfg.dataRealtimeKey;
    } else {
        cfg.dataTopic = cfg.dataStatTopic;
        cfg.dataTag = cfg.dataStatTag;
        cfg.dataKey = cfg.dataStatKey;
    }
    cfg.traceTopic = getText(hwnd, IDC_TRACE_TOPIC);
    cfg.traceTag = getText(hwnd, IDC_TRACE_TAG);
    cfg.traceKey = getText(hwnd, IDC_TRACE_KEY);
    cfg.xmlPath = getText(hwnd, IDC_XML_PATH);
    cfg.jsonDataPath = getText(hwnd, IDC_JSON_PATH);
    cfg.jsonDataPaths = splitPathList(cfg.jsonDataPath);
    cfg.outputPath = getText(hwnd, IDC_OUTPUT_PATH);

    cfg.originPaths = splitPathList(getText(hwnd, IDC_ORIGIN_PATHS));
    return cfg;
}

int toInt(const std::string& s) {
    try {
        return s.empty() ? 0 : std::stoi(s);
    } catch (...) {
        return 0;
    }
}

NodeRef* makeRef(AppState& app, int device, int monitor) {
    auto ref = std::make_unique<NodeRef>();
    ref->device = device;
    ref->monitor = monitor;
    NodeRef* p = ref.get();
    app.refs.push_back(std::move(ref));
    return p;
}

JsonNodeRef* makeJsonRef(AppState& app, int index) {
    auto ref = std::make_unique<JsonNodeRef>();
    ref->index = index;
    JsonNodeRef* p = ref.get();
    app.jsonRefs.push_back(std::move(ref));
    return p;
}

HTREEITEM insertTreeItemRaw(HWND tree, HTREEITEM parent, const std::wstring& text, LPARAM ref) {
    TVINSERTSTRUCTW tv{};
    tv.hParent = parent;
    tv.hInsertAfter = TVI_LAST;
    tv.item.mask = TVIF_TEXT | TVIF_PARAM;
    tv.item.pszText = const_cast<wchar_t*>(text.c_str());
    tv.item.lParam = ref;
    return (HTREEITEM)SendMessageW(tree, TVM_INSERTITEMW, 0, (LPARAM)&tv);
}

HTREEITEM insertTreeItem(AppState& app, HTREEITEM parent, const std::wstring& text, NodeRef* ref) {
    return insertTreeItemRaw(app.tree, parent, text, (LPARAM)ref);
}

HTREEITEM insertJsonTreeItem(AppState& app, HTREEITEM parent, const std::wstring& text, JsonNodeRef* ref) {
    return insertTreeItemRaw(app.jsonTree, parent, text, (LPARAM)ref);
}

std::wstring deviceLabel(const PqLedgerDevice& d) {
    {
        std::wstring text = L"\x88C5\x7F6E ";
        text += u8w(d.name.empty() ? d.id : d.name);
        text += L"  id=" + u8w(d.id);
        text += L"  type=" + u8w(d.devType);
        return text;
    }
    std::wstring text = L"装置 ";
    text += u8w(d.name.empty() ? d.id : d.name);
    text += L"  id=" + u8w(d.id);
    text += L"  type=" + u8w(d.devType);
    return text;
}

std::wstring monitorLabel(const PqLedgerMonitor& m) {
    {
        std::wstring text = L"\x6D4B\x70B9 ";
        text += u8w(m.name.empty() ? m.id : m.name);
        text += L"  id=" + u8w(m.id);
        return text;
    }
    std::wstring text = L"测点 ";
    text += u8w(m.name.empty() ? m.id : m.name);
    text += L"  id=" + u8w(m.id);
    return text;
}

std::wstring processLabel(int processNo) {
    return L"\x8FDB\x7A0B " + std::to_wstring(processNo);
}

void populateTree(AppState& app) {
    TreeView_DeleteAllItems(app.tree);
    app.refs.clear();
    app.selectedDevice = -1;
    app.selectedMonitor = -1;

    int maxProcess = 0;
    for (const auto& d : app.devices) {
        maxProcess = std::max(maxProcess, toInt(d.processNo));
        maxProcess = std::max(maxProcess, toInt(d.maxProcessNum));
    }
    if (!app.devices.empty() && maxProcess < 2) maxProcess = 2;

    std::map<int, HTREEITEM> processNodes;
    for (int i = 1; i <= maxProcess; ++i) {
        processNodes[i] = insertTreeItem(app, TVI_ROOT, processLabel(i), makeRef(app, -1, -1));
    }

    for (size_t i = 0; i < app.devices.size(); ++i) {
        const auto& d = app.devices[i];
        int proc = toInt(d.processNo);
        if (proc <= 0) proc = 1;
        if (!processNodes.count(proc)) {
            processNodes[proc] = insertTreeItem(app, TVI_ROOT, processLabel(proc), makeRef(app, -1, -1));
        }
        HTREEITEM devItem = insertTreeItem(app, processNodes[proc], deviceLabel(d), makeRef(app, (int)i, -1));
        for (size_t j = 0; j < d.monitors.size(); ++j) {
            insertTreeItem(app, devItem, monitorLabel(d.monitors[j]), makeRef(app, (int)i, (int)j));
        }
        TreeView_Expand(app.tree, processNodes[proc], TVE_EXPAND);
        TreeView_Expand(app.tree, devItem, TVE_EXPAND);
    }
}

void appendFields(std::wstring& text, const std::vector<std::pair<std::string, std::string>>& fields) {
    for (const auto& kv : fields) {
        text += u8w(kv.first) + L": " + u8w(kv.second) + L"\r\n";
    }
}

void updateDetails(AppState& app) {
    {
        std::wstring text;
        if (app.selectedDevice >= 0 && app.selectedDevice < (int)app.devices.size()) {
            const auto& d = app.devices[app.selectedDevice];
            if (app.selectedMonitor >= 0 && app.selectedMonitor < (int)d.monitors.size()) {
                const auto& m = d.monitors[app.selectedMonitor];
                text += L"\x6D4B\x70B9\x53F0\x8D26\x4FE1\x606F\r\n";
                text += L"\x6240\x5C5E\x88C5\x7F6E: " + u8w(d.name) + L"\r\n";
                text += L"\x6240\x5C5E\x88C5\x7F6EID: " + u8w(d.id) + L"\r\n";
                text += L"\x6240\x5C5E\x8FDB\x7A0B\x53F7: " + u8w(d.processNo) + L"\r\n";
                text += L"------------------------------\r\n";
                if (!m.fields.empty()) appendFields(text, m.fields);
                else {
                    text += L"id: " + u8w(m.id) + L"\r\n";
                    text += L"name: " + u8w(m.name) + L"\r\n";
                    text += L"lineNo: " + u8w(m.lineNo) + L"\r\n";
                    text += L"voltageLevel: " + u8w(m.voltageLevel) + L"\r\n";
                    text += L"ptType: " + u8w(m.connectType) + L"\r\n";
                    text += L"status: " + u8w(m.status) + L"\r\n";
                }
            } else {
                text += L"\x88C5\x7F6E\x53F0\x8D26\x4FE1\x606F\r\n";
                if (!d.fields.empty()) appendFields(text, d.fields);
                else {
                    text += L"id: " + u8w(d.id) + L"\r\n";
                    text += L"name: " + u8w(d.name) + L"\r\n";
                    text += L"ip: " + u8w(d.ip) + L"\r\n";
                    text += L"status: " + u8w(d.status) + L"\r\n";
                    text += L"devType: " + u8w(d.devType) + L"\r\n";
                    text += L"processNo: " + u8w(d.processNo) + L"\r\n";
                    text += L"maxProcessNum: " + u8w(d.maxProcessNum) + L"\r\n";
                }
                text += L"monitorDataCount: " + std::to_wstring(d.monitors.size()) + L"\r\n";
            }
        } else {
            text = L"\x8BF7\x9009\x62E9\x5DE6\x4FA7\x6811\x4E2D\x7684\x88C5\x7F6E\x6216\x6D4B\x70B9\x3002\x70B9\x51FB\x88C5\x7F6E\x663E\x793A\x88C5\x7F6E\x53F0\x8D26\x4FE1\x606F\xFF0C\x70B9\x51FB\x6D4B\x70B9\x663E\x793A\x6D4B\x70B9\x53F0\x8D26\x4FE1\x606F\x3002";
        }
        SetWindowTextW(app.detail, text.c_str());
        return;
    }
    std::wstring text;
    if (app.selectedDevice >= 0 && app.selectedDevice < (int)app.devices.size()) {
        const auto& d = app.devices[app.selectedDevice];
        if (app.selectedMonitor >= 0 && app.selectedMonitor < (int)d.monitors.size()) {
            const auto& m = d.monitors[app.selectedMonitor];
            text += L"测点台账信息\r\n";
            text += L"所属装置: " + u8w(d.name) + L"\r\n";
            text += L"所属装置ID: " + u8w(d.id) + L"\r\n";
            text += L"所属进程号: " + u8w(d.processNo) + L"\r\n";
            text += L"------------------------------\r\n";
            if (!m.fields.empty()) appendFields(text, m.fields);
            else {
                text += L"id: " + u8w(m.id) + L"\r\n";
                text += L"name: " + u8w(m.name) + L"\r\n";
                text += L"lineNo: " + u8w(m.lineNo) + L"\r\n";
                text += L"voltageLevel: " + u8w(m.voltageLevel) + L"\r\n";
                text += L"ptType: " + u8w(m.connectType) + L"\r\n";
                text += L"status: " + u8w(m.status) + L"\r\n";
            }
        } else {
            text += L"装置台账信息\r\n";
            if (!d.fields.empty()) appendFields(text, d.fields);
            else {
                text += L"id: " + u8w(d.id) + L"\r\n";
                text += L"name: " + u8w(d.name) + L"\r\n";
                text += L"ip: " + u8w(d.ip) + L"\r\n";
                text += L"status: " + u8w(d.status) + L"\r\n";
                text += L"devType: " + u8w(d.devType) + L"\r\n";
                text += L"processNo: " + u8w(d.processNo) + L"\r\n";
                text += L"maxProcessNum: " + u8w(d.maxProcessNum) + L"\r\n";
            }
            text += L"monitorDataCount: " + std::to_wstring(d.monitors.size()) + L"\r\n";
        }
    } else {
        text = L"请选择左侧树中的装置或测点。点击装置显示装置台账信息，点击测点显示测点台账信息。";
    }
    SetWindowTextW(app.detail, text.c_str());
}

std::string prettyJsonText(const std::string& text) {
    try {
        return json::parse(text).dump(2);
    } catch (...) {
        return text;
    }
}

std::string jsonScalarString(const json& value) {
    if (value.is_string()) return value.get<std::string>();
    if (value.is_number_integer()) return std::to_string(value.get<long long>());
    if (value.is_number_unsigned()) return std::to_string(value.get<unsigned long long>());
    if (value.is_number_float()) {
        std::ostringstream oss;
        oss << value.get<double>();
        return oss.str();
    }
    if (value.is_boolean()) return value.get<bool>() ? "true" : "false";
    return value.dump();
}

std::string normalizeDataTypeText(std::string dataType) {
    dataType.erase(std::remove_if(dataType.begin(), dataType.end(),
                                  [](unsigned char c) { return std::isspace(c) != 0; }),
                   dataType.end());
    if (dataType.size() == 1 && std::isdigit(static_cast<unsigned char>(dataType[0]))) {
        dataType = "0" + dataType;
    }
    return dataType;
}

bool findDataTypeInJson(const json& value, std::string& out) {
    if (value.is_object()) {
        auto it = value.find("DATA_TYPE");
        if (it != value.end()) {
            out = normalizeDataTypeText(jsonScalarString(*it));
            return true;
        }
        for (auto iter = value.begin(); iter != value.end(); ++iter) {
            if (findDataTypeInJson(iter.value(), out)) return true;
        }
    } else if (value.is_array()) {
        for (const auto& item : value) {
            if (findDataTypeInJson(item, out)) return true;
        }
    }
    return false;
}

std::string dataTypeFromJsonText(const std::string& text) {
    try {
        json root = json::parse(text);
        std::string dataType;
        if (findDataTypeInJson(root, dataType)) return dataType;
    } catch (...) {
    }
    return "";
}

void appendOriginPathToUi(AppState& app, const std::string& path) {
    if (path.empty()) return;
    std::vector<std::string> paths = splitPathList(getText(app.hwnd, IDC_ORIGIN_PATHS));
    if (std::find(paths.begin(), paths.end(), path) == paths.end()) {
        paths.push_back(path);
        setText(app.hwnd, IDC_ORIGIN_PATHS, joinPaths(paths));
    }
}

void appendJsonPathToUi(AppState& app, const std::string& path) {
    if (path.empty()) return;
    std::vector<std::string> paths = splitPathList(getText(app.hwnd, IDC_JSON_PATH));
    if (std::find(paths.begin(), paths.end(), path) == paths.end()) {
        paths.push_back(path);
        setText(app.hwnd, IDC_JSON_PATH, joinPaths(paths));
    }
}

std::string normalizedPathPrefix(std::string path) {
    try {
        path = fs::absolute(fs::u8path(path)).lexically_normal().u8string();
    } catch (...) {
    }
    for (char& ch : path) {
        if (ch == '/') ch = '\\';
    }
    return path;
}

bool pathIsUnderDir(const std::string& filePath, const std::string& dirPath) {
    if (filePath.empty() || dirPath.empty()) return true;
    std::string file = normalizedPathPrefix(filePath);
    std::string dir = normalizedPathPrefix(dirPath);
    if (file == dir) return true;
    if (!dir.empty() && dir.back() != '\\') dir += '\\';
    return file.rfind(dir, 0) == 0;
}

void rebuildJsonTree(AppState& app);
void updateJsonDetail(AppState& app);

void resetJsonSession(AppState& app, const std::string& traceDir) {
    app.activeTraceDir = traceDir;
    app.jsonItems.clear();
    app.jsonRefs.clear();
    app.selectedJson = -1;
    if (app.hwnd) {
        setText(app.hwnd, IDC_JSON_PATH, "");
        setText(app.hwnd, IDC_ORIGIN_PATHS, "");
    }
    rebuildJsonTree(app);
    updateJsonDetail(app);
}

std::wstring jsonGroupTitle(const std::string& kind) {
    if (kind == "sent") return L"\x6784\x9020\x548C\x53D1\x9001\x7684JSON";
    if (kind == "data") return L"\x6536\x5230\x7684" L"DATATopic JSON";
    return L"TraceTopic\x6536\x5230\x7684JSON\x5217\x8868";
}

void updateJsonDetail(AppState& app) {
    std::wstring text;
    if (app.selectedJson >= 0 && app.selectedJson < (int)app.jsonItems.size()) {
        const auto& item = app.jsonItems[app.selectedJson];
        text += L"title: " + u8w(item.title) + L"\r\n";
        text += L"type: " + u8w(item.kind) + L"\r\n";
        if (!item.path.empty()) text += L"path: " + u8w(item.path) + L"\r\n";
        text += L"------------------------------\r\n";
        text += u8w(prettyJsonText(item.body));
    } else {
        text = L"\x8BF7\x5728\x5DE6\x4FA7\x9009\x62E9JSON\x8BB0\x5F55\x3002";
    }
    if (app.jsonDetail) SetWindowTextW(app.jsonDetail, text.c_str());
}

void rebuildJsonTree(AppState& app) {
    if (!app.jsonTree) return;
    TreeView_DeleteAllItems(app.jsonTree);
    app.jsonRefs.clear();

    HTREEITEM sentRoot = insertJsonTreeItem(app, TVI_ROOT, jsonGroupTitle("sent"), makeJsonRef(app, -1));
    HTREEITEM dataRoot = insertJsonTreeItem(app, TVI_ROOT, jsonGroupTitle("data"), makeJsonRef(app, -1));
    HTREEITEM traceRoot = insertJsonTreeItem(app, TVI_ROOT, jsonGroupTitle("trace"), makeJsonRef(app, -1));

    for (size_t i = 0; i < app.jsonItems.size(); ++i) {
        const auto& item = app.jsonItems[i];
        HTREEITEM parent = item.kind == "sent" ? sentRoot : item.kind == "data" ? dataRoot : traceRoot;
        insertJsonTreeItem(app, parent, u8w(item.title), makeJsonRef(app, (int)i));
    }
    TreeView_Expand(app.jsonTree, sentRoot, TVE_EXPAND);
    TreeView_Expand(app.jsonTree, dataRoot, TVE_EXPAND);
    TreeView_Expand(app.jsonTree, traceRoot, TVE_EXPAND);
}

void addJsonEvent(AppState& app, const PqMqEvent& ev) {
    if (!app.activeTraceDir.empty() && !ev.path.empty() && !pathIsUnderDir(ev.path, app.activeTraceDir)) {
        return;
    }
    JsonItem item{ev.kind, ev.title, ev.body, ev.path, ""};
    if (item.kind.empty()) item.kind = "trace";
    if (item.title.empty()) item.title = item.kind;
    if (item.kind == "data") {
        appendJsonPathToUi(app, item.path);
        item.dataType = dataTypeFromJsonText(item.body);
        if (item.dataType.empty()) item.dataType = "unknown";
        for (const auto& existing : app.jsonItems) {
            if (existing.kind == "data" && existing.dataType == item.dataType) {
                return;
            }
        }
        item.title += " DATA_TYPE=" + item.dataType;
    } else if (item.kind == "trace") {
        appendOriginPathToUi(app, item.path);
    }
    app.jsonItems.push_back(std::move(item));
    app.selectedJson = (int)app.jsonItems.size() - 1;
    rebuildJsonTree(app);
    updateJsonDetail(app);
}

PqMqEventFn makeMqEventCallback(AppState& app) {
    HWND hwnd = app.hwnd;
    return [hwnd](const PqMqEvent& ev) {
        auto* copy = new PqMqEvent(ev);
        if (!PostMessageW(hwnd, WM_PQ_MQ_EVENT, 0, (LPARAM)copy)) delete copy;
    };
}

std::string selectedFrontType(AppState& app) {
    int idx = (int)SendMessageW(app.frontType, CB_GETCURSEL, 0, 0);
    wchar_t buf[128]{};
    SendMessageW(app.frontType, CB_GETLBTEXT, idx < 0 ? 0 : idx, (LPARAM)buf);
    return w8(buf);
}

bool openFileDialog(HWND owner, const wchar_t* title, const wchar_t* filter, std::string& out) {
    wchar_t file[4096]{};
    OPENFILENAMEW ofn{};
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = owner;
    ofn.lpstrTitle = title;
    ofn.lpstrFilter = filter;
    ofn.lpstrFile = file;
    ofn.nMaxFile = (DWORD)std::size(file);
    ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
    if (!GetOpenFileNameW(&ofn)) return false;
    out = w8(file);
    return true;
}

std::vector<std::string> collectOriginFiles(const fs::path& dir) {
    std::vector<std::string> files;
    if (!fs::exists(dir)) return files;
    for (const auto& entry : fs::directory_iterator(dir)) {
        if (!entry.is_regular_file()) continue;
        std::string name = entry.path().filename().u8string();
        if (name.rfind("origin", 0) == 0 && entry.path().extension() == ".txt") {
            files.push_back(entry.path().u8string());
        }
    }
    std::sort(files.begin(), files.end());
    return files;
}

std::vector<std::string> collectJsonDataFiles(const fs::path& dir) {
    std::vector<std::string> files;
    if (!fs::exists(dir)) return files;
    for (const auto& entry : fs::directory_iterator(dir)) {
        if (!entry.is_regular_file()) continue;
        std::string name = entry.path().filename().u8string();
        if (name.rfind("jsondata", 0) == 0 &&
            (entry.path().extension() == ".txt" || entry.path().extension() == ".json")) {
            files.push_back(entry.path().u8string());
        }
    }
    std::sort(files.begin(), files.end());
    return files;
}

std::string joinPaths(const std::vector<std::string>& paths) {
    std::string out;
    for (const auto& p : paths) {
        if (!out.empty()) out += "\r\n";
        out += p;
    }
    return out;
}

void prepareTraceWorkspace(AppState& app,
                           PqToolConfig& cfg,
                           const PqLedgerDevice& d,
                           const PqLedgerMonitor& m,
                           bool updateUi) {
    fs::path dir = fs::u8path(pqTraceDirectoryFor(cfg, d, m));
    fs::create_directories(dir);

    std::string xmlPath = pqFindDeviceXmlPath(cfg, d);
    if (!xmlPath.empty()) cfg.xmlPath = xmlPath;
    cfg.jsonDataPaths = collectJsonDataFiles(dir);
    cfg.jsonDataPath = joinPaths(cfg.jsonDataPaths);
    cfg.originPaths = collectOriginFiles(dir);
    cfg.outputPath = (dir / "final_sorted.xls").u8string();

    if (updateUi) {
        if (!cfg.xmlPath.empty()) setText(app.hwnd, IDC_XML_PATH, cfg.xmlPath);
        setText(app.hwnd, IDC_JSON_PATH, cfg.jsonDataPath);
        setText(app.hwnd, IDC_ORIGIN_PATHS, joinPaths(cfg.originPaths));
        setText(app.hwnd, IDC_OUTPUT_PATH, cfg.outputPath);
    }
}

void updatePathsForSelection(AppState& app) {
    if (app.selectedDevice < 0 || app.selectedDevice >= (int)app.devices.size()) return;
    PqToolConfig cfg = readConfig(app.hwnd);
    const auto& d = app.devices[app.selectedDevice];
    if (app.selectedMonitor >= 0 && app.selectedMonitor < (int)d.monitors.size()) {
        prepareTraceWorkspace(app, cfg, d, d.monitors[app.selectedMonitor], true);
        return;
    }
    std::string xmlPath = pqFindDeviceXmlPath(cfg, d);
    if (!xmlPath.empty()) setText(app.hwnd, IDC_XML_PATH, xmlPath);
}

void showPage(AppState& app, int index) {
    ShowWindow(app.pageConfig, index == 0 ? SW_SHOW : SW_HIDE);
    ShowWindow(app.pageLedger, index == 1 ? SW_SHOW : SW_HIDE);
    ShowWindow(app.pageJson, index == 2 ? SW_SHOW : SW_HIDE);
}

void runGetLedger(AppState& app) {
    try {
        PqToolConfig cfg = readConfig(app.hwnd);
        appendLogW(app, L"开始获取台账和 ICD/XML...");
        std::string firstXml;
        pqFetchLedgerAndIcd(cfg, app.devices, firstXml, [&](const std::string& s) { appendLog(app, s); });
        populateTree(app);
        updateDetails(app);
        if (!firstXml.empty()) setText(app.hwnd, IDC_XML_PATH, firstXml);
        appendLogW(app, L"台账树已刷新。");
    } catch (const std::exception& e) {
        appendLog(app, std::string("获取台账失败: ") + e.what());
        MessageBoxW(app.hwnd, u8w(e.what()).c_str(), L"获取台账失败", MB_ICONERROR);
    }
}

void runInitMq(AppState& app) {
    try {
        PqToolConfig cfg = readConfig(app.hwnd);
        pqInitializeMq(cfg,
                       [&](const std::string& s) { appendLog(app, s); },
                       makeMqEventCallback(app));
    } catch (const std::exception& e) {
        appendLog(app, std::string("初始化 MQ 失败: ") + e.what());
    }
}

void runTrace(AppState& app) {
    try {
        if (app.selectedDevice < 0 || app.selectedMonitor < 0) {
            MessageBoxW(app.hwnd, L"请先在左侧选择一个测点，选中装置或进程时不会发送数据追踪消息。", L"数据追踪", MB_ICONINFORMATION);
            return;
        }
        PqToolConfig cfg = readConfig(app.hwnd);
        const auto& d = app.devices[app.selectedDevice];
        const auto& m = d.monitors[app.selectedMonitor];
        std::string traceDir = pqTraceDirectoryFor(cfg, d, m);
        resetJsonSession(app, traceDir);
        pqClearTraceRecords(cfg, d, m);
        appendLog(app, "已清空当前测点旧文本记录: " + traceDir);
        prepareTraceWorkspace(app, cfg, d, m, true);
        pqSendTraceRequest(cfg, d, m, selectedFrontType(app),
                           [&](const std::string& s) { appendLog(app, s); },
                           makeMqEventCallback(app));
    } catch (const std::exception& e) {
        appendLog(app, std::string("数据追踪失败: ") + e.what());
    }
}

void runParse(AppState& app) {
    PqToolConfig cfg = readConfig(app.hwnd);
    std::string err;
    appendLogW(app, L"开始解析生成表格...");
    std::vector<std::string> jsonPaths = cfg.jsonDataPaths.empty() ? splitPathList(cfg.jsonDataPath) : cfg.jsonDataPaths;
    bool ok = pqRunParser(cfg.xmlPath, jsonPaths, cfg.originPaths, cfg.outputPath, &err, selectedFrontType(app));
    if (ok) {
        appendLog(app, "表格生成完成: " + cfg.outputPath);
        MessageBoxW(app.hwnd, u8w("表格生成完成: " + cfg.outputPath).c_str(), L"完成", MB_ICONINFORMATION);
    } else {
        appendLog(app, "表格生成失败: " + err);
        MessageBoxW(app.hwnd, u8w(err).c_str(), L"表格生成失败", MB_ICONERROR);
    }
}

void createConfigPage(AppState& app) {
    PqToolConfig cfg = pqDefaultConfig();
    HWND p = app.pageConfig;
    addLabel(app, p, L"配置项", 22, 10, 120, 26);
    const int groupX = 18, groupW = 1110, rowGap = 32, x1 = 38, x2 = 520, x3 = 810;

    addGroup(app, p, L"前置配置", groupX, 42, groupW, 82);
    addLabeledEdit(app, p, L"FrontInst", IDC_FRONT_INST, cfg.frontInst, x1, 74, 118, 330);
    addLabeledEdit(app, p, L"FrontIP", IDC_FRONT_IP, cfg.frontIp, x2, 74, 90, 180);

    addGroup(app, p, L"台账配置", groupX, 136, groupW, 82);
    addLabeledEdit(app, p, L"TerminalStatus", IDC_TERMINAL_STATUS, cfg.terminalStatus, x1, 168, 118, 140);
    addLabeledEdit(app, p, L"MonitorStatus", IDC_MONITOR_STATUS, cfg.monitorStatus, x2, 168, 118, 140);
    addLabeledEdit(app, p, L"IcdFlag", IDC_ICD_FLAG, cfg.icdFlag, x3, 168, 70, 80);

    addGroup(app, p, L"接口配置", groupX, 230, groupW, 122);
    addLabeledEdit(app, p, L"WebDevice", IDC_WEB_DEVICE, cfg.webDevice, x1, 262, 118, 520);
    addLabeledEdit(app, p, L"WebIcd", IDC_WEB_ICD, cfg.webIcd, x3, 262, 90, 225);
    addLabeledEdit(app, p, L"WebFiledownload", IDC_WEB_FILEDOWNLOAD, cfg.webFiledownload, x1, 296, 118, 520);
    addLabeledEdit(app, p, L"XML目录", IDC_XML_SAVE_DIR, cfg.xmlSaveDir, x3, 296, 90, 225);

    addGroup(app, p, L"MQ配置", groupX, 364, groupW, 316);
    addLabel(app, p, L"生产者", x1, 398, 80, 22);
    addLabeledEdit(app, p, L"producer", IDC_PRODUCER, cfg.producer, 120, 398, 90, 300);
    addLabeledEdit(app, p, L"Ipport", IDC_IPPORT, cfg.ipport, 600, 398, 80, 260);
    addLabeledEdit(app, p, L"AccessKey", IDC_PRODUCER_ACCESS_KEY, cfg.producerAccessKey, 120, 398 + rowGap, 90, 300);
    addLabeledEdit(app, p, L"SecretKey", IDC_PRODUCER_SECRET_KEY, cfg.producerSecretKey, 600, 398 + rowGap, 80, 260);
    addLabeledEdit(app, p, L"TopicLOG", IDC_TOPIC_LOG, cfg.topicLOG, 120, 398 + rowGap * 2, 90, 360);
    addLabeledEdit(app, p, L"TagLOG", IDC_TAG_LOG, cfg.tagLOG, 575, 398 + rowGap * 2, 75, 270);
    addLabeledEdit(app, p, L"KeyLOG", IDC_KEY_LOG, cfg.keyLOG, 940, 398 + rowGap * 2, 70, 120);

    addLabel(app, p, L"消费者", x1, 512, 80, 22);
    addLabeledEdit(app, p, L"consumer", IDC_CONSUMER, cfg.consumer, 120, 512, 90, 300);
    addLabeledEdit(app, p, L"Ipport", IDC_CONSUMER_IPPORT, cfg.consumerIpport, 600, 512, 80, 260);
    addLabeledEdit(app, p, L"AccessKey", IDC_CONSUMER_ACCESS_KEY, cfg.consumerAccessKey, 120, 512 + rowGap, 90, 300);
    addLabeledEdit(app, p, L"SecretKey", IDC_CONSUMER_SECRET_KEY, cfg.consumerSecretKey, 600, 512 + rowGap, 80, 260);
    addLabeledEdit(app, p, L"\x7EDF\x8BA1Topic", IDC_DATA_STAT_TOPIC, cfg.dataStatTopic, 120, 512 + rowGap * 2, 90, 360);
    addLabeledEdit(app, p, L"DATATag", IDC_DATA_STAT_TAG, cfg.dataStatTag, 575, 512 + rowGap * 2, 75, 270);
    addLabeledEdit(app, p, L"DATAKey", IDC_DATA_STAT_KEY, cfg.dataStatKey, 940, 512 + rowGap * 2, 70, 120);
    addLabeledEdit(app, p, L"\x5B9E\x65F6Topic", IDC_DATA_REALTIME_TOPIC, cfg.dataRealtimeTopic, 120, 512 + rowGap * 3, 90, 360);
    addLabeledEdit(app, p, L"DATATag", IDC_DATA_REALTIME_TAG, cfg.dataRealtimeTag, 575, 512 + rowGap * 3, 75, 270);
    addLabeledEdit(app, p, L"DATAKey", IDC_DATA_REALTIME_KEY, cfg.dataRealtimeKey, 940, 512 + rowGap * 3, 70, 120);
    addLabeledEdit(app, p, L"TraceTopic", IDC_TRACE_TOPIC, cfg.traceTopic, 120, 512 + rowGap * 4, 90, 360);
    addLabeledEdit(app, p, L"TraceTag", IDC_TRACE_TAG, cfg.traceTag, 575, 512 + rowGap * 4, 75, 270);
    addLabeledEdit(app, p, L"TraceKey", IDC_TRACE_KEY, cfg.traceKey, 940, 512 + rowGap * 4, 70, 120);
}

void createLedgerPage(AppState& app) {
    PqToolConfig cfg = pqDefaultConfig();
    HWND p = app.pageLedger;
    int y = 16;
    addLabeledEdit(app, p, L"XML文件", IDC_XML_PATH, cfg.xmlPath, 18, y, 70, 670);
    addButton(app, p, IDC_BROWSE_XML, L"选择XML", 780, y - 1, 90, 25);
    addLabeledEdit(app, p, L"输出", IDC_OUTPUT_PATH, cfg.outputPath, 890, y, 48, 235);
    y += 34;
    addLabel(app, p, L"jsondata", 18, y, 70, 24);
    addMemo(app, p, IDC_JSON_PATH, 88, y, 670, 52);
    setText(p, IDC_JSON_PATH, cfg.jsonDataPath);
    addButton(app, p, IDC_BROWSE_JSON, L"添加JSON", 780, y - 1, 90, 25);
    y += 66;
    addLabel(app, p, L"origin", 18, y, 70, 24);
    addMemo(app, p, IDC_ORIGIN_PATHS, 88, y, 670, 56);
    setText(p, IDC_ORIGIN_PATHS, "origin.txt");
    addButton(app, p, IDC_ADD_ORIGIN, L"添加origin", 780, y - 1, 90, 25);

    y += 70;
    addLabel(app, p, L"进程类型", 18, y, 70, 24);
    app.frontType = CreateWindowExW(0, L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP,
                                    88, y, 150, 120, p, (HMENU)(INT_PTR)IDC_FRONT_TYPE, app.inst, nullptr);
    applyFont(app, app.frontType);
    SendMessageW(app.frontType, CB_ADDSTRING, 0, (LPARAM)L"cfg_stat_data");
    SendMessageW(app.frontType, CB_ADDSTRING, 0, (LPARAM)L"cfg_3s_data");
    SendMessageW(app.frontType, CB_SETCURSEL, 0, 0);
    addButton(app, p, IDC_GET_LEDGER, L"获取台账", 260, y - 1, 100, 26);
    addButton(app, p, IDC_INIT_MQ, L"初始化MQ", 375, y - 1, 100, 26);
    addButton(app, p, IDC_TRACE, L"数据追踪", 490, y - 1, 100, 26);
    addButton(app, p, IDC_PARSE, L"一键解析", 605, y - 1, 100, 26);

    int panelY = y + 42;
    app.tree = CreateWindowExW(WS_EX_CLIENTEDGE, WC_TREEVIEWW, L"",
                               WS_CHILD | WS_VISIBLE | WS_TABSTOP | TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS,
                               18, panelY, 430, 455, p, (HMENU)(INT_PTR)IDC_TREE, app.inst, nullptr);
    applyFont(app, app.tree);
    app.detail = addMemo(app, p, IDC_DETAIL, 465, panelY, 670, 155);
    SendMessageW(app.detail, EM_SETREADONLY, TRUE, 0);
    app.log = addMemo(app, p, IDC_LOG, 465, panelY + 170, 670, 285);
    SendMessageW(app.log, EM_SETREADONLY, TRUE, 0);
    updateDetails(app);
    appendLogW(app, L"工具已启动。无参数运行显示界面；带 XML 参数运行原命令行解析。");
}

void createJsonPage(AppState& app) {
    HWND p = app.pageJson;
    addLabel(app, p, L"JSON\x8BB0\x5F55", 18, 14, 130, 24);
    addLabel(app, p, L"JSON\x8BE6\x60C5", 385, 14, 130, 24);
    app.jsonTree = CreateWindowExW(WS_EX_CLIENTEDGE, WC_TREEVIEWW, L"",
                                   WS_CHILD | WS_VISIBLE | WS_TABSTOP |
                                       TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS,
                                   18, 45, 345, 590, p, (HMENU)(INT_PTR)IDC_JSON_TREE, app.inst, nullptr);
    applyFont(app, app.jsonTree);
    app.jsonDetail = addMemo(app, p, IDC_JSON_DETAIL, 385, 45, 750, 590);
    SendMessageW(app.jsonDetail, EM_SETREADONLY, TRUE, 0);
    rebuildJsonTree(app);
    updateJsonDetail(app);
}

void createConfigPageClean(AppState& app) {
    PqToolConfig cfg = pqDefaultConfig();
    HWND p = app.pageConfig;
    const int groupX = 18, groupW = 1110, rowGap = 32, x1 = 38, x2 = 520, x3 = 810;
    const int mqNameX = 38, mqFieldX = 160, mqTopicW = 310, mqTagX = 575, mqTagW = 250, mqKeyX = 930, mqKeyW = 120;

    addLabel(app, p, L"\x914D\x7F6E\x9879", 22, 10, 120, 26);

    addGroup(app, p, L"\x524D\x7F6E\x914D\x7F6E", groupX, 42, groupW, 82);
    addLabeledEdit(app, p, L"FrontInst", IDC_FRONT_INST, cfg.frontInst, x1, 74, 118, 330);
    addLabeledEdit(app, p, L"FrontIP", IDC_FRONT_IP, cfg.frontIp, x2, 74, 90, 180);

    addGroup(app, p, L"\x53F0\x8D26\x914D\x7F6E", groupX, 136, groupW, 82);
    addLabeledEdit(app, p, L"TerminalStatus", IDC_TERMINAL_STATUS, cfg.terminalStatus, x1, 168, 118, 140);
    addLabeledEdit(app, p, L"MonitorStatus", IDC_MONITOR_STATUS, cfg.monitorStatus, x2, 168, 118, 140);
    addLabeledEdit(app, p, L"IcdFlag", IDC_ICD_FLAG, cfg.icdFlag, x3, 168, 70, 80);

    addGroup(app, p, L"\x63A5\x53E3\x914D\x7F6E", groupX, 230, groupW, 122);
    addLabeledEdit(app, p, L"WebDevice", IDC_WEB_DEVICE, cfg.webDevice, x1, 262, 118, 520);
    addLabeledEdit(app, p, L"WebIcd", IDC_WEB_ICD, cfg.webIcd, x3, 262, 90, 225);
    addLabeledEdit(app, p, L"WebFiledownload", IDC_WEB_FILEDOWNLOAD, cfg.webFiledownload, x1, 296, 118, 520);
    addLabeledEdit(app, p, L"XML\x76EE\x5F55", IDC_XML_SAVE_DIR, cfg.xmlSaveDir, x3, 296, 90, 225);

    addGroup(app, p, L"MQ\x914D\x7F6E", groupX, 364, groupW, 316);
    addLabel(app, p, L"\x751F\x4EA7\x8005", mqNameX, 398, 90, 22);
    addLabeledEdit(app, p, L"producer", IDC_PRODUCER, cfg.producer, mqFieldX, 398, 90, 300);
    addLabeledEdit(app, p, L"Ipport", IDC_IPPORT, cfg.ipport, mqTagX, 398, 80, 260);
    addLabeledEdit(app, p, L"AccessKey", IDC_PRODUCER_ACCESS_KEY, cfg.producerAccessKey, mqFieldX, 398 + rowGap, 90, 300);
    addLabeledEdit(app, p, L"SecretKey", IDC_PRODUCER_SECRET_KEY, cfg.producerSecretKey, mqTagX, 398 + rowGap, 80, 260);
    addLabeledEdit(app, p, L"TopicLOG", IDC_TOPIC_LOG, cfg.topicLOG, mqFieldX, 398 + rowGap * 2, 90, mqTopicW);
    addLabeledEdit(app, p, L"TagLOG", IDC_TAG_LOG, cfg.tagLOG, mqTagX, 398 + rowGap * 2, 75, mqTagW);
    addLabeledEdit(app, p, L"KeyLOG", IDC_KEY_LOG, cfg.keyLOG, mqKeyX, 398 + rowGap * 2, 70, mqKeyW);

    addLabel(app, p, L"\x6D88\x8D39\x8005", mqNameX, 512, 90, 22);
    addLabeledEdit(app, p, L"consumer", IDC_CONSUMER, cfg.consumer, mqFieldX, 512, 90, 300);
    addLabeledEdit(app, p, L"Ipport", IDC_CONSUMER_IPPORT, cfg.consumerIpport, mqTagX, 512, 80, 260);
    addLabeledEdit(app, p, L"AccessKey", IDC_CONSUMER_ACCESS_KEY, cfg.consumerAccessKey, mqFieldX, 512 + rowGap, 90, 300);
    addLabeledEdit(app, p, L"SecretKey", IDC_CONSUMER_SECRET_KEY, cfg.consumerSecretKey, mqTagX, 512 + rowGap, 80, 260);
    addLabeledEdit(app, p, L"\x7EDF\x8BA1Topic", IDC_DATA_STAT_TOPIC, cfg.dataStatTopic, mqFieldX, 512 + rowGap * 2, 90, mqTopicW);
    addLabeledEdit(app, p, L"DATATag", IDC_DATA_STAT_TAG, cfg.dataStatTag, mqTagX, 512 + rowGap * 2, 75, mqTagW);
    addLabeledEdit(app, p, L"DATAKey", IDC_DATA_STAT_KEY, cfg.dataStatKey, mqKeyX, 512 + rowGap * 2, 70, mqKeyW);
    addLabeledEdit(app, p, L"\x5B9E\x65F6Topic", IDC_DATA_REALTIME_TOPIC, cfg.dataRealtimeTopic, mqFieldX, 512 + rowGap * 3, 90, mqTopicW);
    addLabeledEdit(app, p, L"DATATag", IDC_DATA_REALTIME_TAG, cfg.dataRealtimeTag, mqTagX, 512 + rowGap * 3, 75, mqTagW);
    addLabeledEdit(app, p, L"DATAKey", IDC_DATA_REALTIME_KEY, cfg.dataRealtimeKey, mqKeyX, 512 + rowGap * 3, 70, mqKeyW);
    addLabeledEdit(app, p, L"TraceTopic", IDC_TRACE_TOPIC, cfg.traceTopic, mqFieldX, 512 + rowGap * 4, 90, mqTopicW);
    addLabeledEdit(app, p, L"TraceTag", IDC_TRACE_TAG, cfg.traceTag, mqTagX, 512 + rowGap * 4, 75, mqTagW);
    addLabeledEdit(app, p, L"TraceKey", IDC_TRACE_KEY, cfg.traceKey, mqKeyX, 512 + rowGap * 4, 70, mqKeyW);
}

void createLedgerPageClean(AppState& app) {
    PqToolConfig cfg = pqDefaultConfig();
    HWND p = app.pageLedger;
    int y = 16;
    addLabeledEdit(app, p, L"XML\x6587\x4EF6", IDC_XML_PATH, cfg.xmlPath, 18, y, 70, 670);
    addButton(app, p, IDC_BROWSE_XML, L"\x9009\x62E9XML", 780, y - 1, 90, 25);
    addLabeledEdit(app, p, L"\x8F93\x51FA", IDC_OUTPUT_PATH, cfg.outputPath, 890, y, 48, 235);
    y += 34;
    addLabel(app, p, L"jsondata", 18, y, 70, 24);
    addMemo(app, p, IDC_JSON_PATH, 88, y, 670, 52);
    setText(p, IDC_JSON_PATH, cfg.jsonDataPath);
    addButton(app, p, IDC_BROWSE_JSON, L"\x6DFB\x52A0JSON", 780, y - 1, 90, 25);
    y += 66;
    addLabel(app, p, L"origin", 18, y, 70, 24);
    addMemo(app, p, IDC_ORIGIN_PATHS, 88, y, 670, 56);
    setText(p, IDC_ORIGIN_PATHS, "origin.txt");
    addButton(app, p, IDC_ADD_ORIGIN, L"\x6DFB\x52A0origin", 780, y - 1, 90, 25);

    y += 70;
    addLabel(app, p, L"\x8FDB\x7A0B\x7C7B\x578B", 18, y, 70, 24);
    app.frontType = CreateWindowExW(0, L"COMBOBOX", L"", WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_TABSTOP,
                                    88, y, 150, 120, p, (HMENU)(INT_PTR)IDC_FRONT_TYPE, app.inst, nullptr);
    applyFont(app, app.frontType);
    SendMessageW(app.frontType, CB_ADDSTRING, 0, (LPARAM)L"cfg_stat_data");
    SendMessageW(app.frontType, CB_ADDSTRING, 0, (LPARAM)L"cfg_3s_data");
    SendMessageW(app.frontType, CB_SETCURSEL, 0, 0);
    addButton(app, p, IDC_GET_LEDGER, L"\x83B7\x53D6\x53F0\x8D26", 260, y - 1, 100, 26);
    addButton(app, p, IDC_INIT_MQ, L"\x521D\x59CB\x5316MQ", 375, y - 1, 100, 26);
    addButton(app, p, IDC_TRACE, L"\x6570\x636E\x8FFD\x8E2A", 490, y - 1, 100, 26);
    addButton(app, p, IDC_PARSE, L"\x4E00\x952E\x89E3\x6790", 605, y - 1, 100, 26);

    int panelY = y + 42;
    app.tree = CreateWindowExW(WS_EX_CLIENTEDGE, WC_TREEVIEWW, L"",
                               WS_CHILD | WS_VISIBLE | WS_TABSTOP | TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS,
                               18, panelY, 430, 430, p, (HMENU)(INT_PTR)IDC_TREE, app.inst, nullptr);
    applyFont(app, app.tree);
    app.detail = addMemo(app, p, IDC_DETAIL, 465, panelY, 670, 150);
    SendMessageW(app.detail, EM_SETREADONLY, TRUE, 0);
    app.log = addMemo(app, p, IDC_LOG, 465, panelY + 165, 670, 265);
    SendMessageW(app.log, EM_SETREADONLY, TRUE, 0);
    updateDetails(app);
    appendLogW(app, L"\x5DE5\x5177\x5DF2\x542F\x52A8\x3002");
}

void createUi(AppState& app) {
    app.font = CreateFontW(-16, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
                           DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
                           CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_SWISS, L"Microsoft YaHei UI");
    app.ownsFont = app.font != nullptr;
    if (!app.font) app.font = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
    app.tab = CreateWindowExW(0, WC_TABCONTROLW, L"", WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_TABSTOP,
                              10, 10, 1170, 735, app.hwnd, (HMENU)(INT_PTR)IDC_TAB, app.inst, nullptr);
    applyFont(app, app.tab);

    TCITEMW item{};
    item.mask = TCIF_TEXT;
    item.pszText = const_cast<wchar_t*>(L"配置项");
    SendMessageW(app.tab, TCM_INSERTITEMW, 0, (LPARAM)&item);
    item.pszText = const_cast<wchar_t*>(L"台账列表");
    SendMessageW(app.tab, TCM_INSERTITEMW, 1, (LPARAM)&item);
    item.pszText = const_cast<wchar_t*>(L"JSON\x8BE6\x60C5");
    SendMessageW(app.tab, TCM_INSERTITEMW, 2, (LPARAM)&item);
    item.pszText = const_cast<wchar_t*>(L"\x914D\x7F6E\x9879");
    SendMessageW(app.tab, TCM_SETITEMW, 0, (LPARAM)&item);
    item.pszText = const_cast<wchar_t*>(L"\x53F0\x8D26\x5217\x8868");
    SendMessageW(app.tab, TCM_SETITEMW, 1, (LPARAM)&item);
    SendMessageW(app.tab, TCM_SETITEMSIZE, 0, MAKELPARAM(150, 34));

    app.pageConfig = CreateWindowExW(0, L"PqToolPanel", L"", WS_CHILD | WS_VISIBLE,
                                     20, 52, 1150, 680, app.hwnd, (HMENU)(INT_PTR)IDC_PAGE_CONFIG, app.inst, nullptr);
    app.pageLedger = CreateWindowExW(0, L"PqToolPanel", L"", WS_CHILD,
                                     20, 52, 1150, 680, app.hwnd, (HMENU)(INT_PTR)IDC_PAGE_LEDGER, app.inst, nullptr);
    app.pageJson = CreateWindowExW(0, L"PqToolPanel", L"", WS_CHILD,
                                   20, 52, 1150, 680, app.hwnd, (HMENU)(INT_PTR)IDC_PAGE_JSON, app.inst, nullptr);
    createConfigPageClean(app);
    createLedgerPageClean(app);
    createJsonPage(app);
    showPage(app, 0);
}

LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
    AppState& app = *g_app;
    switch (msg) {
    case WM_PQ_MQ_EVENT: {
        std::unique_ptr<PqMqEvent> ev(reinterpret_cast<PqMqEvent*>(lp));
        if (ev) addJsonEvent(app, *ev);
        return 0;
    }
    case WM_CREATE:
        app.hwnd = hwnd;
        createUi(app);
        return 0;
    case WM_COMMAND:
        if (LOWORD(wp) == IDC_BROWSE_JSON) {
            std::string path;
            if (openFileDialog(hwnd, L"\x6DFB\x52A0 jsondata \x6587\x4EF6", L"Text/JSON Files\0*.txt;*.json\0All Files\0*.*\0", path)) {
                std::vector<std::string> paths = splitPathList(getText(hwnd, IDC_JSON_PATH));
                if (std::find(paths.begin(), paths.end(), path) == paths.end()) paths.push_back(path);
                setText(hwnd, IDC_JSON_PATH, joinPaths(paths));
            }
            return 0;
        }
        switch (LOWORD(wp)) {
        case IDC_GET_LEDGER: runGetLedger(app); return 0;
        case IDC_INIT_MQ: runInitMq(app); return 0;
        case IDC_TRACE: runTrace(app); return 0;
        case IDC_PARSE: runParse(app); return 0;
        case IDC_BROWSE_XML: {
            std::string path;
            if (openFileDialog(hwnd, L"选择 XML 文件", L"XML Files\0*.xml\0All Files\0*.*\0", path)) setText(hwnd, IDC_XML_PATH, path);
            return 0;
        }
        case IDC_BROWSE_JSON: {
            std::string path;
            if (openFileDialog(hwnd, L"选择 jsondata.txt", L"Text/JSON Files\0*.txt;*.json\0All Files\0*.*\0", path)) setText(hwnd, IDC_JSON_PATH, path);
            return 0;
        }
        case IDC_ADD_ORIGIN: {
            std::string path;
            if (openFileDialog(hwnd, L"选择 origin 文件", L"Text/JSON Files\0*.txt;*.json\0All Files\0*.*\0", path)) {
                std::string old = getText(hwnd, IDC_ORIGIN_PATHS);
                if (!old.empty() && old.back() != '\n' && old.back() != '\r') old += "\r\n";
                setText(hwnd, IDC_ORIGIN_PATHS, old + path);
            }
            return 0;
        }
        }
        break;
    case WM_NOTIFY:
        if (((LPNMHDR)lp)->idFrom == IDC_TAB && ((LPNMHDR)lp)->code == TCN_SELCHANGE) {
            showPage(app, (int)SendMessageW(app.tab, TCM_GETCURSEL, 0, 0));
            return 0;
        }
        if (((LPNMHDR)lp)->idFrom == IDC_TREE && ((LPNMHDR)lp)->code == TVN_SELCHANGEDW) {
            auto* tv = (LPNMTREEVIEWW)lp;
            auto* ref = (NodeRef*)tv->itemNew.lParam;
            if (ref) {
                app.selectedDevice = ref->device;
                app.selectedMonitor = ref->monitor;
                updateDetails(app);
                updatePathsForSelection(app);
            }
            return 0;
        }
        if (((LPNMHDR)lp)->idFrom == IDC_JSON_TREE && ((LPNMHDR)lp)->code == TVN_SELCHANGEDW) {
            auto* tv = (LPNMTREEVIEWW)lp;
            auto* ref = (JsonNodeRef*)tv->itemNew.lParam;
            app.selectedJson = ref ? ref->index : -1;
            updateJsonDetail(app);
            return 0;
        }
        break;
    case WM_DESTROY:
        if (app.ownsFont && app.font) DeleteObject(app.font);
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProcW(hwnd, msg, wp, lp);
}

} // namespace

int runPqGuiApp() {
    AppState app;
    g_app = &app;
    app.inst = GetModuleHandleW(nullptr);
    CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);

    INITCOMMONCONTROLSEX icc{};
    icc.dwSize = sizeof(icc);
    icc.dwICC = ICC_TREEVIEW_CLASSES | ICC_TAB_CLASSES | ICC_STANDARD_CLASSES;
    InitCommonControlsEx(&icc);

    WNDCLASSW wc{};
    wc.hInstance = app.inst;
    wc.lpszClassName = L"PqToolWindow";
    wc.lpfnWndProc = wndProc;
    wc.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512));
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    RegisterClassW(&wc);

    WNDCLASSW panel{};
    panel.hInstance = app.inst;
    panel.lpszClassName = L"PqToolPanel";
    panel.lpfnWndProc = panelProc;
    panel.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512));
    panel.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    RegisterClassW(&panel);

    HWND hwnd = CreateWindowExW(0, wc.lpszClassName, L"PQ 映射解析与追踪工具",
                                WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 1210, 820,
                                nullptr, nullptr, app.inst, nullptr);
    if (!hwnd) {
        CoUninitialize();
        return 1;
    }
    SetWindowTextW(hwnd, L"PQ \x6620\x5C04\x89E3\x6790\x4E0E\x8FFD\x8E2A\x5DE5\x5177");
    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd);

    MSG message;
    while (GetMessageW(&message, nullptr, 0, 0) > 0) {
        TranslateMessage(&message);
        DispatchMessageW(&message);
    }

    CoUninitialize();
    g_app = nullptr;
    return (int)message.wParam;
}
