package com.evobms.project.iot.controller;

import com.alibaba.fastjson2.JSON;
import com.evobms.common.constant.BboxApiConstants;
import com.evobms.common.constant.Constants;
import com.evobms.common.utils.StringUtils;
import com.evobms.common.utils.AesEcbPkcs5Utils;
import com.evobms.project.iot.service.DeviceTokenService;
import com.evobms.project.system.domain.BmsDevices;
import com.evobms.project.system.service.IBmsDevicesService;
import com.evobms.project.ota.service.IOtaTasksService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URI;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 设备鉴权令牌接口
 * 路径：POST /iot/auth/token
 */
@RestController
@Tag(name = "设备鉴权", description = "设备鉴权令牌接口")
@Api(tags = "IoT设备鉴权")
@RequestMapping("/iot")
@Slf4j
public class IotAuthController {
    private static final Logger log = LoggerFactory.getLogger(IotAuthController.class);

    @Autowired(required = false)
    private IBmsDevicesService bmsDevicesService;

    @Autowired
    private DeviceTokenService deviceTokenService;

    @Value("${token.header:Authorization}")
    private String tokenHeader;

    @Value("${mqtt.host:tcp://192.168.5.23:1883}")
    private String mqttHostUri;
    @Value("${mqtt.username:00000001}")
    private String mqttUsername;
    @Value("${mqtt.password:741a03a10f3de6b2}")
    private String mqttPassword;

    @Autowired(required = false)
    private IOtaTasksService otaTasksService;

    @Value("${ota.count:001}")
    private String otaCount;

    // OTA文件存储位置：支持 classpath 或 文件系统目录，例如：classpath:/ota 或 /data/ota
    @Value("${ota.repo:D:/OTA}")
    private String otaRepo;

    // OTA分片大小（字节），规范为1KB=1024字节
    @Value("${ota.chunk-size:1024}")
    private int otaChunkSize;
    // 说明：首片头摘要计算基于“首片1024字节数据”；此项仅控制头部编码模式
    @Value("${ota.header-format:binary32}")
    private String otaHeaderFormat;

    /**
     * 设备请求令牌
     * 请求体：{ sn, device, version }
     * 返回：{ code, success, token }
     */
    @PostMapping("/auth/token")
    @Operation(summary = "设备请求令牌", description = "通过SN码，设备号，版本信息生成TOKEN")
    @ApiOperation("设备请求令牌")
    public Map<String, Object> requestToken(@RequestBody Map<String, String> body) {
        String sn = safeTrim(body.get("sn"));
        String model = safeTrim(body.get("device"));
        String version = safeTrim(body.get("version"));
        log.info("设备请求:{}",body.toString());
        log.info("sn:{}, device:{}, version:{}", sn, model, version);
        MDC.put("sn", sn);
        MDC.put("model", model);
        MDC.put("version", version);

        Map<String, Object> resp = new HashMap<>();

        try {
            if (StringUtils.isEmpty(sn)) {
                resp.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
                resp.put("success", "0");
                return resp;
            }

            // 根据规范：sn 作为设备唯一编码，这里映射为平台系统侧 BBOX 管理中的 deviceId
            BmsDevices device = null;
            if (bmsDevicesService != null) {
                BmsDevices query = new BmsDevices();
                // 依据设备上报的SN，使用系统侧BmsDevices的deviceSn字段进行校验
                query.setDeviceSn(sn);
                device = bmsDevicesService.selectBmsDevicesList1(query);

            }

            if (device == null) {
                log.warn("设备未注册: sn={}", sn);
                resp.put("code", BboxApiConstants.CODE_DEVICE_NOT_REGISTERED);
                resp.put("success", "0");
                return resp;
            }

            if ("1".equals(device.getStatus())) { // 1=停用/禁用
                log.warn("设备已被禁用: sn={}", sn);
                resp.put("code", BboxApiConstants.CODE_DEVICE_DISABLED);
                resp.put("success", "0");
                return resp;
            }

            // 设备合法，改为JWT令牌，长度固定65
            String token = deviceTokenService.issueTokenForSn(sn);
            log.info("BBOX设备token: {}", token);
            log.info("设备令牌签发成功: sn={} model={} version={} tokenLen={}", sn, model, version, token != null ? token.length() : 0);
            resp.put("code", BboxApiConstants.CODE_SUCCESS);
            resp.put("success", "1");
            resp.put("token", token);
            return resp;
        } catch (Exception e) {
            log.error("设备令牌签发异常: {}", e.getMessage(), e);
            resp.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
            resp.put("success", "0");
            return resp;
        } finally {
            MDC.clear();
        }
    }

    private static String safeTrim(String s) {
        return s == null ? null : s.trim();
    }

    /**
     * 获取设备端 MQTT 登录信息
     * 类型：GET，请求头须带鉴权令牌：`Authorization: Bearer <token>`
     * 路径：/iot/auth/mqtt
     * 请求参数：version（可选）
     * 返回：{ code, success, data(Base64) }
     */
    @Operation(summary = " MQTT 登录", description = "获取设备端 MQTT 登录信息")
    @ApiOperation(value = "MQTT 登录", notes = "获取设备端 MQTT 登录信息")
    @GetMapping("/auth/mqtt")
    public Map<String, Object> getMqttCredentials(@RequestParam(value = "version", required = false) String version,
                                                  HttpServletRequest request) {
        Map<String, Object> resp = new HashMap<>();
        try {
            // 解析设备令牌，获取设备SN（非JWT）
            String sn = deviceTokenService.resolveSn(request);
            if (StringUtils.isEmpty(sn)) {
                resp.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
                resp.put("success", "0");
                return resp;
            }

            // 解析 MQTT 主机与端口
            String host = "192.168.5.23";
            int port = 1883;
            try {
                URI uri = new URI(mqttHostUri);
                if (uri.getHost() != null) {
                    host = uri.getHost();
                }
                if (uri.getPort() != -1) {
                    port = uri.getPort();
                }
            } catch (Exception ignore) {
            }

            String use = fixLengthRight(mqttUsername, 8);
            String pass = fixLengthRight(mqttPassword, 16);

            Map<String, String> data = new HashMap<>();
            data.put("clientid", sn);
            data.put("host", host);
            data.put("port", String.valueOf(port));
            data.put("user", use);
            data.put("passwd", pass);

            String json = JSON.toJSONString(data);
            log.debug("登录mqtt信息====》{}", json);
            // AES密钥：sn后补0到16位
            String encryptedBase64;
            if (sn.equals("EVO0001")) {
                 String key161 = fixLengthRight("EVO0001", 16);
                 encryptedBase64 = AesEcbPkcs5Utils.encryptToAscII16(json, key161);
            } else if (sn.equals("EVO0002")) {
                String key161 = fixLengthRight("EVO0002", 16);
                encryptedBase64 = AesEcbPkcs5Utils.encryptToAscII16(json, key161);
            }else {
                String keyTail6 = sn.length() >= 16 ? sn.substring(sn.length() - 16) : sn;
                encryptedBase64 = AesEcbPkcs5Utils.encryptToAscII16(json, keyTail6);
            }
            log.debug("登录mqtt信息加密===================》{}", encryptedBase64);
            resp.put("code", BboxApiConstants.CODE_SUCCESS);
            resp.put("success", "1");
            resp.put("data", encryptedBase64);
            return resp;
        } catch (Exception e) {
            resp.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
            resp.put("success", "0");
            return resp;
        }
    }

    /**
     * 设备登录并上报固件版本，服务端判断是否需要OTA升级
     * 类型：POST，请求头须带鉴权令牌：`Authorization: Bearer <token>`
     * 路径：/iot/auth/login
     * 请求参数(body)：{ "version":"1.0.0" }
     * 返回：{ code(3位), success(1位), update(1位), version(5位), url(11位), count(3位) }
     */
    @ApiOperation(value = "设备登录", notes = "设备登录并上报固件版本，服务端判断是否需要OTA升级")
    @PostMapping("/auth/login")
    public Map<String, Object> deviceLogin(@RequestBody(required = false) String body, @RequestParam(value = "version", required = false) String versionParam, HttpServletRequest request) {
        Map<String, Object> resp = new HashMap<>();
        try {
            // 解析自定义令牌获取设备SN
            String sn = deviceTokenService.resolveSn(request);
            if (StringUtils.isEmpty(sn)) {
                resp.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
                resp.put("success", "0");
                return resp;
            }
            String currVersion = safeTrim(versionParam);
            if (StringUtils.isEmpty(currVersion) && body != null) {
                String raw = body.trim();
                try {
                    Map<String, String> m = JSON.parseObject(raw, Map.class);
                    currVersion = safeTrim(m.get("version"));
                } catch (Exception ignore) {
                    int idxEq = raw.indexOf('=');
                    int idxColon = raw.indexOf(':');
                    if (StringUtils.isEmpty(currVersion) && idxEq != -1) {
                        String key = safeTrim(raw.substring(0, idxEq));
                        String val = safeTrim(raw.substring(idxEq + 1));
                        if ("version".equalsIgnoreCase(key)) currVersion = val;
                    }
                    if (StringUtils.isEmpty(currVersion) && idxColon != -1) {
                        String key = safeTrim(raw.substring(0, idxColon));
                        String val = safeTrim(raw.substring(idxColon + 1));
                        if ("version".equalsIgnoreCase(key)) currVersion = val;
                    }
                    if (StringUtils.isEmpty(currVersion) && raw.matches("[0-9]+(\\.[0-9]+)*")) {
                        currVersion = raw;
                    }
                }
            }
            MDC.put("sn", sn);
            MDC.put("version", currVersion);

            if (StringUtils.isEmpty(sn)) {
                resp.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
                resp.put("success", "0");
                return resp;
            }

            // 查询设备信息
            BmsDevices device = null;
            if (bmsDevicesService != null) {
                BmsDevices query = new BmsDevices();
                query.setDeviceSn(sn);
                 device = bmsDevicesService.selectBmsDevicesList1(query);

            }

            if (device == null) {
                log.warn("设备未注册: sn={}", sn);
                resp.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
                resp.put("success", "0");
                return resp;
            }

            // 目标升级版本：直接取设备记录中的固件版本（跳过 OTA 任务查询）
            String targetVersion = null;
            try {
                targetVersion = device.getFirmwareVersion();
            } catch (Exception ignore) {

            }

            // 版本比较，决定是否更新
            boolean needUpdate = false;
            if (!StringUtils.isEmpty(targetVersion)) {
                if (StringUtils.isEmpty(currVersion)) {
                    needUpdate = true;
                } else {
                    needUpdate = isVersionGreater(targetVersion, currVersion);
                }
            }

            String update = needUpdate ? "1" : "0";
            String retVersion = fixOrTrimRight(needUpdate ? targetVersion : StringUtils.defaultString(currVersion), 5);
            // 固件下载起始路径（设备从 000 分片开始下载），相对路径模式
            String url = "/iot/ota/000";
            // count 为分片总数，3 位字符串。优先动态计算；无法读取固件时回退至配置；不更新返回 "000"
            String count;
            if (needUpdate) {
                // 若存在目标版本，对应固件可用时，动态计算分片总数（总长度含 64 字节校验头）；否则回退配置值
                Integer dynamicCount = computeOtaChunkCount(targetVersion);
                if (dynamicCount != null && dynamicCount > 0) {
                    // 转为 3 位字符串（左侧补零），例如 7 -> "007"
                    count = fixLengthLeft(String.valueOf(dynamicCount), 3);
                } else {
                    // 回退使用配置 `ota.count`，默认 "001"
                    count = fixLengthLeft(StringUtils.defaultString(otaCount), 3);
                }
            } else {
                // 不需要更新，设备无需下载，返回 "000"
                count = fixLengthLeft("000", 3);
            }
            // 详细日志
            log.info("设备登录决策: sn={} currVersion={} targetVersion={} needUpdate={} count={}",
                    sn, currVersion, targetVersion, needUpdate, count);

            resp.put("code", BboxApiConstants.CODE_SUCCESS);
            resp.put("success", "1");
            resp.put("update", update);
            resp.put("version", retVersion);
            resp.put("url", url);
            resp.put("count", count);
            return resp;
        } catch (Exception e) {
            resp.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
            resp.put("success", "0");
            return resp;
        }
    }

    // 语义化版本比较：target > current 返回 true
    private static boolean isVersionGreater(String target, String current) {
        try {
            String[] ta = StringUtils.defaultString(target).split("\\.");
            String[] ca = StringUtils.defaultString(current).split("\\.");
            int len = Math.max(ta.length, ca.length);
            for (int i = 0; i < len; i++) {
                int tv = i < ta.length ? Integer.parseInt(ta[i]) : 0;
                int cv = i < ca.length ? Integer.parseInt(ca[i]) : 0;
                if (tv != cv) return tv > cv;
            }
            return false; // 完全相等
        } catch (Exception e) {
            // 解析异常时，仅判断不相等则视为需要更新
            return !StringUtils.equals(StringUtils.defaultString(target), StringUtils.defaultString(current));
        }
    }

    /**
     * OTA 固件分片下载接口（仅 binary32）
     * - 鉴权：请求头需携带 `Authorization: Bearer <token>`（与设备登录相同）
     * - 路径：`/iot/ota/{num}`，num 为三位数字字符串（000 起始）
     * - 参数：`version`（必填，目标固件版本号，例如 `1.0.1`）
     * - 响应：
     * 成功：`application/octet-stream` 二进制数据；每次返回最多 1024 字节（最后一片可能不足 1024）
     * 失败：`{ code, success }` JSON，错误码沿用 `BboxApiConstants`
     * - 首片摘要计算：针对首片1024字节数据生成摘要并作为校验头：32字节SHA-256原始摘要
     * - 首片结构：`[header] + [首片数据(最多1024)]`，总长 `headerLen + 1024`
     * - 后续分片：与首片相同，每片前置相同模式的校验头（摘要基于该分片的数据），随后紧跟该分片数据
     */
    @GetMapping("/ota/{num}")
    @Operation(summary = "OTA固件分片下载", description = " OTA 固件分片下载接口")
    @ApiOperation("OTA固件分片下载")
    public Object downloadOtaChunk(@PathVariable("num") String num,
                                   @RequestParam(value = "version", required = false) String version,
                                   HttpServletRequest request) {
        Map<String, Object> err = new HashMap<>();
        try {
            // BBOX设备鉴权
            String sn = deviceTokenService.resolveSn(request);
            if (StringUtils.isEmpty(sn)) {
                log.warn("OTA下载未授权: Authorization 缺失或无效");
                err.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
                err.put("success", "0");
                return err;
            }
            MDC.put("sn", sn);
            MDC.put("version", version);

            if (StringUtils.isEmpty(version)) {
                //固件文件不存在
                log.warn("OTA下载版本缺失: sn={}", sn);
                err.put("code", BboxApiConstants.CODE_FILE_NOT_FOUND);
                err.put("success", "0");
                return err;
            }

            // 加载固件并构造带校验头的负载
            byte[] firmware = loadFirmwareBytes(version);
            if (firmware == null || firmware.length == 0) {
                log.warn("OTA固件不存在: sn={} version={} repo={}", sn, version, otaRepo);
                err.put("code", BboxApiConstants.CODE_FILE_NOT_FOUND);
                err.put("success", "0");
                return err;
            }
            // 校验分片编号
            if (StringUtils.isEmpty(num) || num.length() != 3) {
                log.warn("OTA分片编号非法: sn={} version={} num={}", sn, version, num);
                err.put("code", BboxApiConstants.CODE_NOT_FOUND);
                err.put("success", "0");
                return err;
            }
            int idx;
            try {
                idx = Integer.parseInt(num);
            } catch (Exception e) {
                log.warn("OTA分片编号解析失败: sn={} version={} num={}", sn, version, num);
                err.put("code", BboxApiConstants.CODE_NOT_FOUND);
                err.put("success", "0");
                return err;
            }

            //计算总分片数
            int firmwareBytes = firmware.length;
            int chunks = (firmwareBytes + otaChunkSize - 1) / otaChunkSize;
            if (idx == 0) {
                try {
                    int previewLen = Math.min(otaChunkSize, firmwareBytes);
                    byte[] preview = Arrays.copyOfRange(firmware, 0, previewLen);
                    // 变更说明：headerSHA256日志打印为“首片1024字节数据”的摘要，而非整包
                    String headerHex = sha256Hex(preview);
                    log.info("OTA下载开始: sn={} version={} headerSHA256={} headerMode={} chunkSize={} firmwareBytes={} chunks={}", sn, version, headerHex, "binary32", otaChunkSize, firmwareBytes, chunks);
                } catch (Exception ignore) {
                    log.info("OTA下载开始: sn={} version={} headerMode={} chunkSize={} firmwareBytes={} chunks={}", sn, version, "binary32", otaChunkSize, firmwareBytes, chunks);
                }
            }

            // 组包分片：首片返回 [header]
            byte[] chunk;
            String mode = "binary32";
            if (idx == 0) {
                // ：头部 + 实际数据（首片不补齐）
                int firstDataLen = Math.min(otaChunkSize, firmwareBytes);
                byte[] firstData = Arrays.copyOfRange(firmware, 0, firstDataLen);
                byte[] header = sha256Bytes(firstData);
                int headerLen = header.length;
                // 合并 header 与首片固件数据，首片总长 = headerLen + firstDataLen
                chunk = new byte[headerLen + firstDataLen];
                System.arraycopy(header, 0, chunk, 0, headerLen);
                System.arraycopy(firstData, 0, chunk, headerLen, firstDataLen);

                log.info("OTA首片长度: sn={} version={} num={} headerMode={} headerLen={} dataLen={} totalLen={}",
                        sn, version, num, mode, headerLen, firstDataLen, chunk.length);
                try {
                    log.info("OTA首片头HEX({}): {}", headerLen, toHex(header));
                    int prefixLen = Math.min(64, firstData.length);
                    log.info("OTA首片数据HEX前{}字节: {}", prefixLen, toHex(Arrays.copyOfRange(firstData, 0, prefixLen)));
                } catch (Exception ignore) {
                }
            } else {
                // 后续分片：从首片实际数据(1024)之后开始切片；并在分片前置校验头（摘要基于该分片数据）
                int firstDataLen = Math.min(otaChunkSize, firmwareBytes);
                int offset = firstDataLen + (idx - 1) * otaChunkSize;
                if (offset >= firmwareBytes) {
                    log.warn("OTA分片越界: sn={} version={} num={} offset={} firmwareBytes={} chunks={}", sn, version, num, offset, firmwareBytes, chunks);
                    err.put("code", BboxApiConstants.CODE_NOT_FOUND);
                    err.put("success", "0");
                    return err;
                }
                int dataLen = Math.min(otaChunkSize, firmwareBytes - offset);
                byte[] data = Arrays.copyOfRange(firmware, offset, offset + dataLen);
                byte[] dataForDigest;
                int dataOutLen;
                if (dataLen < otaChunkSize) {
                    dataForDigest = new byte[otaChunkSize];
                    System.arraycopy(data, 0, dataForDigest, 0, dataLen);
                    dataOutLen = otaChunkSize;
                } else {
                    dataForDigest = data;
                    dataOutLen = dataLen;
                }
                byte[] header = sha256Bytes(dataForDigest);
                int headerLen = header.length;
                chunk = new byte[headerLen + dataOutLen];
                System.arraycopy(header, 0, chunk, 0, headerLen);
                System.arraycopy(dataForDigest, 0, chunk, headerLen, dataOutLen);
                log.info("OTA分片长度: sn={} version={} num={} headerMode={} headerLen={} dataLen={} totalLen={}", sn, version, num, mode, headerLen, dataOutLen, chunk.length);
                try {
                    log.info("OTA分片头HEX({}): {}", headerLen, toHex(header));
                    int prefixLen = Math.min(64, dataForDigest.length);
                    log.info("OTA分片数据HEX前{}字节: {}", prefixLen, toHex(Arrays.copyOfRange(dataForDigest, 0, prefixLen)));
                } catch (Exception ignore) {
                }
            }
            log.debug("OTA分片发送: sn={} version={} num={} chunkLen={}",
                    sn, version, num, chunk.length);

            return ResponseEntity
                    .ok()
                    .contentType(MediaType.APPLICATION_OCTET_STREAM)
                    .contentLength(chunk.length)
                    .body(chunk);
        } catch (Exception e) {
            log.error("OTA分片下载异常: {}", e.getMessage(), e);
            err.put("code", BboxApiConstants.CODE_UNAUTHORIZED);
            err.put("success", "0");
            return err;
        } finally {
            MDC.clear();
        }
    }

    /**
     * 动态计算 OTA 分片总数
     * - 规则：仅按固件数据长度计算（不含首片校验头）
     * - 分片：按 `ota.chunk-size`（默认 1024 字节）切分，向上取整
     * - 返回：无法读取固件或异常时返回 null
     */
    private Integer computeOtaChunkCount(String version) {
        try {
            if (StringUtils.isEmpty(version)) return null;
            byte[] firmware = loadFirmwareBytes(version);
            if (firmware == null || firmware.length == 0) return null;
            int firmwareBytes = firmware.length;
            int chunks = (firmwareBytes + otaChunkSize - 1) / otaChunkSize;
            return Math.max(chunks, 1);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 构造 OTA 负载（仅 binary32）
     * - 格式：`[32字节SHA256原始摘要] + [固件]`
     * - 摘要口径：统一基于“首片1024字节数据”，与分片接口首片一致
     */
    private byte[] buildOtaPayload(byte[] firmware) throws Exception {
        int firstDataLen = Math.min(otaChunkSize, firmware.length);
        byte[] firstData = Arrays.copyOfRange(firmware, 0, firstDataLen);
        byte[] digest = sha256Bytes(firstData);
        byte[] payload = new byte[digest.length + firmware.length];
        System.arraycopy(digest, 0, payload, 0, digest.length);
        System.arraycopy(firmware, 0, payload, digest.length, firmware.length);
        return payload;
    }

    /**
     * 读取固件字节
     * - 支持：`classpath:` 或 文件系统目录
     * - 映射：固件文件命名为 `<version>.bin`，目录为 `ota.repo` 指定位置
     * - 返回：文件不存在或访问失败返回 null
     */
    private byte[] loadFirmwareBytes(String version) {
        try {
            if (otaRepo.startsWith("classpath:")) {
                String base = otaRepo.substring("classpath:".length());
                if (!base.endsWith("/")) base = base + "/";
                String rel = base + version + ".bin";
                Resource res = new ClassPathResource(rel);
                if (!res.exists()) return null;
                try (InputStream in = res.getInputStream();
                     ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
                    byte[] buf = new byte[4096];
                    int r;
                    while ((r = in.read(buf)) != -1) bos.write(buf, 0, r);
                    return bos.toByteArray();
                }
            } else {
                String base = otaRepo;
                if (!base.endsWith("/")) base = base + "/";
                Path p = Paths.get(base + version + ".bin");
                if (!Files.exists(p)) return null;
                return Files.readAllBytes(p);
            }
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * 计算 SHA256 的十六进制字符串
     * - 输出：小写，长度 64 的 ASCII 字符串
     * - 输入：固件原始字节
     */
    private static String sha256Hex(byte[] data) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] digest = md.digest(data);
        StringBuilder sb = new StringBuilder(digest.length);
        for (byte b : digest) {
            //转为 16 进制字符串
            String s = Integer.toHexString(0xff & b);
            if (s.length() == 1) sb.append('0');
            sb.append(s);
        }
        return sb.toString();
    }

    private static byte[] sha256Bytes(byte[] data) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        return md.digest(data);
    }

    private static String toHex(byte[] data) {
        if (data == null) return "";
        StringBuilder sb = new StringBuilder(data.length * 2);
        for (byte b : data) {
            String s = Integer.toHexString(0xff & b);
            if (s.length() == 1) sb.append('0');
            sb.append(s);
        }
        return sb.toString();
    }

    private static String fixLengthRight(String s, int len) {
        if (s == null) s = "";
        if (s.length() >= len){
            return s.substring(0, len);
        }
        StringBuilder sb = new StringBuilder(len);
        sb.append(s);
        while (sb.length() < len) sb.append('0');
        return sb.toString();
    }

    private static String fixLengthLeft(String s, int len) {
        if (s == null) s = "";
        if (s.length() >= len) return s.substring(s.length() - len);
        StringBuilder sb = new StringBuilder(len);
        while (sb.length() < len - s.length()) sb.append('0');
        sb.append(s);
        return sb.toString();
    }

    private static String fixOrTrimRight(String s, int len) {
        if (s == null) s = "";
        if (s.length() == len) return s;
        if (s.length() > len) return s.substring(0, len);
        StringBuilder sb = new StringBuilder(len);
        sb.append(s);
        while (sb.length() < len) sb.append('0');
        return sb.toString();
    }
}
