// HjyIot.cpp - 贺简云物联 IoT 设备接入库实现（ESP32 Arduino C++）
// 仅依赖 ESP32 Arduino 核心内置组件（WiFi / HTTPClient / Update）。

#include "HjyIot.h"
#include "HjyIotSha256.h"
#include <Update.h>

// ---------------------------------------------------------------------------
// 内部工具：URL 解码（仅处理 %XX，用于响应头 X-OTA-Version，服务端用 rawurlencode）
// ---------------------------------------------------------------------------
static String urlDecode(const String& in) {
    String out;
    out.reserve(in.length());
    for (unsigned int i = 0; i < in.length(); i++) {
        char c = in[i];
        if (c == '%' && i + 2 < in.length()) {
            auto hexVal = [](char h) -> int {
                if (h >= '0' && h <= '9') return h - '0';
                if (h >= 'a' && h <= 'f') return h - 'a' + 10;
                if (h >= 'A' && h <= 'F') return h - 'A' + 10;
                return -1;
            };
            int hi = hexVal(in[i + 1]);
            int lo = hexVal(in[i + 2]);
            if (hi >= 0 && lo >= 0) {
                out += (char)((hi << 4) | lo);
                i += 2;
                continue;
            }
        }
        out += c;
    }
    return out;
}

// ---------------------------------------------------------------------------
// 内部工具：极简 base64（用于 HTTP Basic 头）
// ---------------------------------------------------------------------------
static const char B64_ALPHABET[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

static String base64Encode(const String& in) {
    String out;
    int len = (int)in.length();
    int i = 0;
    for (; i + 2 < len; i += 3) {
        uint32_t triple = ((uint8_t)in[i] << 16) | ((uint8_t)in[i + 1] << 8) | (uint8_t)in[i + 2];
        out += B64_ALPHABET[(triple >> 18) & 0x3F];
        out += B64_ALPHABET[(triple >> 12) & 0x3F];
        out += B64_ALPHABET[(triple >> 6) & 0x3F];
        out += B64_ALPHABET[triple & 0x3F];
    }
    if (len - i == 1) {
        uint32_t triple = (uint8_t)in[i] << 16;
        out += B64_ALPHABET[(triple >> 18) & 0x3F];
        out += B64_ALPHABET[(triple >> 12) & 0x3F];
        out += "==";
    } else if (len - i == 2) {
        uint32_t triple = ((uint8_t)in[i] << 16) | ((uint8_t)in[i + 1] << 8);
        out += B64_ALPHABET[(triple >> 18) & 0x3F];
        out += B64_ALPHABET[(triple >> 12) & 0x3F];
        out += B64_ALPHABET[(triple >> 6) & 0x3F];
        out += '=';
    }
    return out;
}

// ---------------------------------------------------------------------------
// 构造 / 析构
// ---------------------------------------------------------------------------
HjyIotDevice::HjyIotDevice(const char* baseUrl, const char* deviceId, const char* deviceToken) {
    _baseUrl = baseUrl ? String(baseUrl) : String("");
    _deviceId = deviceId ? String(deviceId) : String("");
    _deviceToken = deviceToken ? String(deviceToken) : String("");
}

HjyIotDevice::~HjyIotDevice() {}

// ---------------------------------------------------------------------------
// 通用请求
// ---------------------------------------------------------------------------
bool HjyIotDevice::request(const String& method, const String& action, const String& body,
                           String& out, unsigned long timeoutMs) {
    if (_baseUrl.length() == 0 || _deviceId.length() == 0 || _deviceToken.length() == 0) {
        out = "{\"code\":-1,\"msg\":\"baseUrl/deviceId/deviceToken 未配置\"}";
        return false;
    }
    String url = _baseUrl;
    if (url.indexOf('?') >= 0) {
        url += "&action=";
    } else {
        url += "?action=";
    }
    url += action;

    bool useTls = url.startsWith("https://");

    WiFiClient tcpClient;
    WiFiClientSecure tlsClient;

    HTTPClient http;
    http.setTimeout(timeoutMs);
    // ESP32 Arduino core 3.x 起 HTTPClient::begin 要求 NetworkClient&，不能再传基类 Client*
    bool begun = false;
    if (useTls) {
        tlsClient.setInsecure();  // 生产建议改为固定 CA 校验：tlsClient.setCACert(ROOT_CA)
        begun = http.begin(tlsClient, url);
    } else {
        begun = http.begin(tcpClient, url);
    }
    if (!begun) {
        out = "{\"code\":-1,\"msg\":\"http.begin 失败\"}";
        return false;
    }
    http.addHeader("Content-Type", "application/json");
    http.addHeader("Authorization", buildAuthHeader(_deviceId.c_str(), _deviceToken.c_str()));

    int httpCode = 0;
    if (body.length() == 0) {
        httpCode = method == "GET" ? http.GET() : http.POST("");
    } else {
        httpCode = method == "GET" ? http.GET() : http.POST(body);
    }

    bool ok = (httpCode > 0);
    out = ok ? http.getString() : String("");
    if (!ok) {
        out = "{\"code\":-1,\"msg\":\"HTTP 请求失败: " + String(httpCode) + "\"}";
    }
    http.end();
    return ok;
}

// ---------------------------------------------------------------------------
// 各业务接口
// ---------------------------------------------------------------------------
bool HjyIotDevice::heartbeat(String& out, unsigned long timeoutMs) {
    return request("POST", "heartbeat", "", out, timeoutMs);
}

bool HjyIotDevice::report(const String& propsJson, String& out, unsigned long timeoutMs) {
    // 推荐结构：{"props":{...},"ts":<unix秒>}；若 propsJson 已是完整对象也可直接传
    String body = "{\"props\":" + propsJson + ",\"ts\":" + String((uint32_t)time(nullptr)) + "}";
    return request("POST", "report", body, out, timeoutMs);
}

bool HjyIotDevice::reportWithFs(const String& propsJson, const String& fsJsonArray,
                                String& out, unsigned long timeoutMs) {
    String body = "{\"props\":" + propsJson + ",\"fs\":" + fsJsonArray +
                  ",\"ts\":" + String((uint32_t)time(nullptr)) + "}";
    return request("POST", "report", body, out, timeoutMs);
}

bool HjyIotDevice::pullCommands(String& out, unsigned long timeoutMs) {
    return request("GET", "command", "", out, timeoutMs);
}

bool HjyIotDevice::diskStats(String& out, unsigned long timeoutMs) {
    return request("GET", "disk_stats", "", out, timeoutMs);
}

bool HjyIotDevice::diskList(const String& path, String& out, unsigned long timeoutMs) {
    String body = path.length() == 0 ? String("{}") : String("{\"path\":\"") + path + "\"}";
    return request("POST", "disk_list", body, out, timeoutMs);
}

bool HjyIotDevice::diskRead(const String& path, String& out, unsigned long timeoutMs) {
    if (path.length() == 0) {
        out = "{\"code\":-1,\"msg\":\"diskRead 需要 path\"}";
        return false;
    }
    String body = String("{\"path\":\"") + path + "\"}";
    return request("POST", "disk_read", body, out, timeoutMs);
}

// ---------------------------------------------------------------------------
// OTA 固件下载：流式写入 OTA 分区 + SHA-256 校验
// 平台侧约束（与后端一致）：OtaModel::tenantEnabled、任务未冻结、状态 ready/pushed、
// 推送后 24h 下载窗口内；成功返回 application/octet-stream，
// 失败才返回 JSON（1002 权限/停用/冻结、1003 任务不存在、1004 参数/未就绪/过期、1006 固件缺失）。
// ---------------------------------------------------------------------------
bool HjyIotDevice::otaDownload(const String& jobId, String& version, size_t& size,
                               String& sha256Hex, String& err, unsigned long timeoutMs) {
    version = "";
    size = 0;
    sha256Hex = "";
    err = "";

    if (jobId.length() == 0) {
        err = "otaDownload 需要 jobId";
        return false;
    }
    if (_baseUrl.length() == 0 || _deviceId.length() == 0 || _deviceToken.length() == 0) {
        err = "baseUrl/deviceId/deviceToken 未配置";
        return false;
    }

    String url = _baseUrl;
    url += (url.indexOf('?') >= 0) ? "&" : "?";
    url += "action=ota_download&job_id=" + jobId;

    WiFiClient tcpClient;
    WiFiClientSecure tlsClient;
    HTTPClient http;
    http.setTimeout(timeoutMs);
    bool begun = false;
    if (url.startsWith("https://")) {
        tlsClient.setInsecure();  // 生产建议改为固定 CA 校验：tlsClient.setCACert(ROOT_CA)
        begun = http.begin(tlsClient, url);
    } else {
        begun = http.begin(tcpClient, url);
    }
    if (!begun) {
        err = "http.begin 失败（URL 或网络异常）";
        return false;
    }
    http.addHeader("Authorization", buildAuthHeader(_deviceId.c_str(), _deviceToken.c_str()));
    http.addHeader("Accept", "application/octet-stream");

    const int code = http.GET();
    if (code != HTTP_CODE_OK) {
        String body = http.getString();
        body.replace("\r", " ");
        body.replace("\n", " ");
        err = "HTTP " + String(code) + (body.length() > 0 ? (": " + body.substring(0, 180)) : String(""));
        http.end();
        return false;
    }

    // 失败路径：平台以 HTTP 200 + JSON 返回业务错误，只有成功才是二进制流
    const String ctype = http.header("Content-Type");
    if (ctype.indexOf("json") >= 0) {
        String body = http.getString();
        err = body.length() > 0 ? body : String("平台返回 JSON 错误（空响应体）");
        http.end();
        return false;
    }

    const int contentLength = http.getSize();
    if (contentLength <= 0) {
        err = "响应缺少 Content-Length，无法流式写入 OTA 分区";
        http.end();
        return false;
    }
    const size_t expectSize = (size_t)contentLength;

    version = urlDecode(http.header("X-OTA-Version"));
    sha256Hex = http.header("X-OTA-SHA256");
    sha256Hex.toLowerCase();
    if (sha256Hex.length() != 64) {
        err = "响应缺少合法的 X-OTA-SHA256，已拒绝写入 OTA 分区";
        http.end();
        return false;
    }

    if (!Update.begin(expectSize)) {
        err = String("OTA 分区不可用或空间不足: ") + Update.errorString();
        http.end();
        return false;
    }

    WiFiClient* stream = http.getStreamPtr();
    HjySha256 ctx;
    uint8_t buf[1024];
    size_t remain = expectSize;
    bool ioErr = false;
    while (remain > 0) {
        const size_t want = (remain > sizeof(buf)) ? sizeof(buf) : remain;
        const int n = stream->readBytes(buf, want);
        if (n <= 0) {
            ioErr = true;
            break;
        }
        if (Update.write(buf, (size_t)n) != (size_t)n) {
            ioErr = true;
            break;
        }
        ctx.update(buf, (size_t)n);
        remain -= (size_t)n;
    }

    uint8_t digest[32];
    char localHex[65];
    ctx.finish(digest);
    HjySha256::hex(digest, localHex);
    const String local(localHex);

    if (ioErr || remain > 0) {
        Update.abort();
        err = "固件下载中断，剩余 " + String((unsigned long)remain) + " 字节，已放弃写入";
        http.end();
        return false;
    }
    if (local != sha256Hex) {
        Update.abort();
        err = "SHA-256 校验不匹配（平台 " + sha256Hex + " / 本地 " + local + "），已放弃写入";
        http.end();
        return false;
    }
    if (!Update.end(true)) {
        err = String("写入 OTA 分区失败: ") + Update.errorString();
        http.end();
        return false;
    }

    size = expectSize;
    http.end();
    return true;
}

// ---------------------------------------------------------------------------
// Basic 鉴权头
// ---------------------------------------------------------------------------
String HjyIotDevice::buildAuthHeader(const char* deviceId, const char* deviceToken) {
    String plain = String(deviceId) + ":" + String(deviceToken);
    return String("Basic ") + base64Encode(plain);
}
