package org.dromara.app.controller;


import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.domain.bo.AppScheduleBo;
import org.dromara.app.domain.bo.AppSchedulingDeviceBo;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.domain.vo.*;
import org.dromara.app.service.*;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.exception.user.UserException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.encrypt.annotation.ApiEncrypt;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.helper.DataPermissionHelper;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysUserBo;
import org.dromara.system.domain.vo.SysOssUploadVo;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.domain.vo.UserInfoVo;
import org.dromara.system.service.ISysOssService;
import org.dromara.system.service.ISysUserService;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;


@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/app/v1")
public class AppController extends BaseController {

    private static final long MAX_IMAGE_SIZE_BYTES = 20L * 1024 * 1024;
    private static final DateTimeFormatter SECOND_TIME_FORMATTER =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final Set<String> ALLOWED_IMAGE_EXTENSIONS =
        Set.of("jpg", "jpeg", "png", "gif", "webp", "bmp");
    private static final Map<String, Set<String>> IMAGE_CONTENT_TYPES = Map.of(
        "jpg", Set.of("image/jpeg", "image/jpg"),
        "png", Set.of("image/png"),
        "gif", Set.of("image/gif"),
        "webp", Set.of("image/webp"),
        "bmp", Set.of("image/bmp", "image/x-bmp", "image/x-ms-bmp")
    );

    private final IAppDeviceService appDeviceService;
    private final IAppScheduleService appScheduleService;
    private final IAppScheduleDetailService appScheduleDetailService;
    private final IAppSchedulingDeviceService appSchedulingDeviceService;
    private final ISysUserService userService;
    private final IAppWateringLogService appWateringLogService;
    private final IDeviceCommandService deviceCommandService;
    private final ISysOssService ossService;
    private final IAppVersionService appVersionService;

    /**
     *查询设备
     *
     */
    @GetMapping("/deviceList")
    public TableDataInfo<AppDeviceVo> list(AppDeviceBo bo, PageQuery pageQuery) {
        bo.setUserId(LoginHelper.getUserId());
        pageQuery.setOrderByColumn("createTime");
        pageQuery.setIsAsc("desc");
        return appDeviceService.queryPageList(bo, pageQuery);
    }



    /**
     * 添加设备
     *
     */
    @ApiEncrypt
    @PostMapping("/bindDeviceStatus")
    public R<Map<String, Object>> bindDeviceStatus(@RequestBody String body) {
        AppDeviceBo appDevice = JsonUtils.parseObject(body, AppDeviceBo.class);
        appDevice.setUserId(LoginHelper.getUserId());
        Map<String, Object> map = appDeviceService.bindDeviceStatus(appDevice);
        return R.ok(map);
    }

    /**
     * 绑定已通过MQTT注册上线的设备
     */
    @ApiEncrypt
    @PostMapping("/addDevice")
    public R<AppDeviceVo> bindDevice(@RequestBody String body) {
        AppDeviceBo appDevice = JsonUtils.parseObject(body, AppDeviceBo.class);
        appDevice.setUserId(LoginHelper.getUserId());
        return R.ok(appDeviceService.bindRegisteredDevice(appDevice));
    }

    /**
     * 获取设备信息
     详细信息
     *
     * @param deviceNo 主键
     */
    @GetMapping("/device/{deviceNo}")
    public R<AppDeviceVo> getDeviceInfo(@NotNull(message = "主键不能为空")
                                  @PathVariable String deviceNo) {
        return R.ok(getOwnedDevice(deviceNo));
    }


    /**
     * 修改设备信息
     *
     */
    @RepeatSubmit()
    @PutMapping("/updateDeviceInfo")
    public R<Void> updateDeviceInfo(@Validated @RequestBody AppDeviceBo bo) {
        assertDeviceOwned(bo.getDeviceNo());
        bo.setUserId(LoginHelper.getUserId());
        return toAjax(appDeviceService.updateByBo(bo));
    }

    /**
     * 删除设备
     */
    @Log(title = "设备信息", businessType = BusinessType.DELETE)
    @DeleteMapping("/deleteDevice/{deviceNos}")
    public R<Void> removeDevice(@NotEmpty(message = "主键不能为空")
                          @PathVariable String[] deviceNos) {
        for (String deviceNo : deviceNos) {
            assertDeviceOwned(deviceNo);
        }
        return toAjax(appDeviceService.unbindDevices(List.of(deviceNos), LoginHelper.getUserId()));
    }


    /**
     * 手动开启关闭设备
     *
     */
    @RepeatSubmit()
    @PutMapping("/switchDevice")
    public R<Void> switchDevice(@RequestBody String body) {
        Map<String, String> map = JsonUtils.parseObject(body, new TypeReference<>() { });
        String deviceNo = map.get("deviceNo");
        String status = map.get("workStatus");
        String startTime = LocalDateTime.now().format(SECOND_TIME_FORMATTER);
        String durationMinValue = map.get("durationMin");
        Integer durationMin = StringUtils.isBlank(durationMinValue) ? null : Integer.valueOf(durationMinValue);
        //更新设备工作状态
        Boolean flag = appDeviceService.switchDevice(deviceNo, status, startTime, durationMin);
        // 记录信息到历史表
        return toAjax(flag);
    }

    /**
     * 排程关联设备查询
     * @param
     * @return
     */
    @GetMapping("/scheduleDeviceList/{scheduleId}")
    public R<Map<String, Object>> scheduleDeviceList(@PathVariable Long scheduleId) {
        Map<String, Object> retMap = new HashMap<>();
        //所有设备列表
        AppDeviceBo appDeviceBo = new AppDeviceBo();
        appDeviceBo.setUserId(LoginHelper.getUserId());
        List<AppDeviceVo> appDeviceVos = appDeviceService.queryList(appDeviceBo);
        List<String> deviceNos = appDeviceVos.stream()
            .map(AppDeviceVo::getDeviceNo)
            .filter(StringUtils::isNotBlank)
            .toList();
        Set<String> boundDeviceNos = appSchedulingDeviceService.findBoundDeviceNos(deviceNos);
        List<AppDeviceVo> appDeviceVoList = appDeviceVos.stream()
            .filter(device -> !boundDeviceNos.contains(device.getDeviceNo()))
            .toList();
        retMap.put("deviceList", appDeviceVoList);
        return R.ok(retMap);
    }
    /**
     *绑定排程跟设备
     */
    @RepeatSubmit()
    @PostMapping("/addScheduleDevice")
    public R<Void> addScheduleDevice( @RequestBody String body) {
        Map<String, Object> map = JsonUtils.parseObject(body, new TypeReference<>() { });
        if (map == null || map.get("scheduleId") == null) {
            throw new ServiceException("app.schedule.id.not.blank.request");
        }
        Long scheduleId = Long.valueOf(map.get("scheduleId").toString());
        assertScheduleOwned(scheduleId);
        List<String> deviceNos = parseDeviceNos(map);
        if (deviceNos.isEmpty()) {
            throw new ServiceException("app.schedule.devices.not.blank");
        }
        List<Map<String, Object>> scheduleDetails = loadScheduleDetailPayload(scheduleId);

        for (String deviceNo:deviceNos){
            if (StringUtils.isBlank(deviceNo)) {
                continue;
            }
            assertDeviceOwned(deviceNo);
            if (appSchedulingDeviceService.queryByScheduleIdAndDeviceNo(scheduleId, deviceNo) != null) {
                continue;
            }
            AppSchedulingDeviceBo schedulingDeviceBo = new AppSchedulingDeviceBo();
            schedulingDeviceBo.setDeviceNo(deviceNo);
            schedulingDeviceBo.setScheduleId(scheduleId);
            if (appSchedulingDeviceService.insertByBo(schedulingDeviceBo)) {
                deviceCommandService.sendScheduleBindCommand(deviceNo,
                    buildScheduleBindPayload(deviceNo, scheduleDetails));
            }
        }
        return R.ok();
    }

    private List<String> parseDeviceNos(Map<String, Object> map) {
        Object value = Optional.ofNullable(map.get("deviceNos"))
            .orElseGet(() -> Optional.ofNullable(map.get("deviceNo")).orElse(map.get("deviceIds")));
        if (value == null) {
            return Collections.emptyList();
        }
        if (value instanceof Collection<?> collection) {
            return collection.stream()
                .filter(Objects::nonNull)
                .map(Object::toString)
                .filter(StringUtils::isNotBlank)
                .toList();
        }
        String text = value.toString();
        if (StringUtils.isBlank(text)) {
            return Collections.emptyList();
        }
        return Arrays.stream(text.split(","))
            .map(String::trim)
            .filter(StringUtils::isNotBlank)
            .toList();
    }

    private Map<String, Object> buildScheduleBindPayload(String deviceNo, List<Map<String, Object>> details) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("deviceNo", deviceNo);
        payload.put("details", details);
        return payload;
    }

    private List<Map<String, Object>> loadScheduleDetailPayload(Long scheduleId) {
        return toScheduleDetailPayload(appScheduleDetailService.queryByScheduleIdByStatus(scheduleId));
    }

    private List<Map<String, Object>> toScheduleDetailPayload(List<AppScheduleDetailVo> details) {
        List<Map<String, Object>> detailPayload = new ArrayList<>();
        if (details == null) {
            return detailPayload;
        }
        for (AppScheduleDetailVo detail : details) {
            List<Object> timeSlots = new ArrayList<>();
            JSONArray timeArray = JSONUtil.parseArray(detail.getTimeData());
            for (Object timeSlot : timeArray) {
                timeSlots.add(timeSlot);
            }

            if (detail.getStatus().equals("1")){
                Map<String, Object> item = new HashMap<>();
                //  item.put("id", detail.getId());
                item.put("weekday", detail.getWeekday());
                item.put("timeData", timeSlots);
                item.put("triggerType", detail.getTriggerType());
                item.put("status", detail.getStatus());
                detailPayload.add(item);
            }

        }
        return detailPayload;
    }

    /**
     * 删除排程与设备绑定
     * @param scheduleId,deviceNo
     * @return
     */

    @Log(title = "排程与设备关联", businessType = BusinessType.DELETE)
    @DeleteMapping("/deleteScheduleDevice")
    public R<Void> deleteScheduleDevice(@RequestParam(required = false) String scheduleId,
                                        @RequestParam(required = false) String deviceNo) {
        if (StringUtils.isBlank(scheduleId)) {
            throw new ServiceException("app.schedule.id.not.blank.request");
        }
        if (StringUtils.isBlank(deviceNo)) {
            throw new ServiceException("app.device.no.not.blank");
        }
        Long scheduleIdValue = Long.valueOf(scheduleId);
        assertScheduleOwned(scheduleIdValue);
        assertDeviceOwned(deviceNo);
        Boolean deleted = appSchedulingDeviceService.deleteWithValidByScheduleIdAndDeviceNo(scheduleIdValue, deviceNo);
        if (deleted) {
            // 通知设备解绑排程
            try {
                deviceCommandService.sendScheduleUnbindCommand(deviceNo, scheduleIdValue);
            } catch (Exception mqttEx) {
                log.warn("[排程通知] 删除排程设备绑定后下发解绑命令失败 排程ID={} 设备编号={} 原因={}",
                    scheduleIdValue, deviceNo, mqttEx.getMessage(), mqttEx);
            }
        }
        return toAjax(deleted);
    }



    /**
     * 查询排程列表
     */
    @GetMapping("/schedulelist")
    public TableDataInfo<AppScheduleVo> schedulelist(AppScheduleBo bo, PageQuery pageQuery) {
        bo.setUserId(LoginHelper.getUserId());
        pageQuery.setOrderByColumn("createTime");
        pageQuery.setIsAsc("desc");
        return appScheduleService.queryPageList(bo, pageQuery);
    }

    /**
     * 新增排程
     *
     */
    @Log(title = "日程排列", businessType = BusinessType.INSERT)
    @RepeatSubmit()
    @PostMapping("/addschedule")
    public R<AppScheduleVo> addSchedule(@Validated(AddGroup.class) @RequestBody AppScheduleBo bo) {
        bo.setCreateTime(new Date());
        AppScheduleVo appScheduleVo = appScheduleService.insertByBo(bo);
        if (ObjectUtil.isNotNull(appScheduleVo)){
            return R.ok(appScheduleVo);
        }
        return R.fail();
    }

    /**
     * 排程詳情
     * @param
     * @param
     * @return
     */
    @GetMapping("/schedule/{id}")
    public R<AppScheduleVo> getScheduleInfo(@NotNull(message = "主键不能为空")
                                    @PathVariable Long id) {
        AppScheduleVo appScheduleVo = getOwnedSchedule(id);
        if (ObjectUtil.isNotNull(appScheduleVo)){
            List<AppScheduleDetailVo> list = appScheduleDetailService.queryByScheduleIdByStatus(appScheduleVo.getId());

            AppSchedulingDeviceBo appSchedulingDeviceBo = new AppSchedulingDeviceBo();
            appSchedulingDeviceBo.setScheduleId(appScheduleVo.getId());
            List<AppSchedulingDeviceVo> appSchedulingDeviceVos = appSchedulingDeviceService.queryList(appSchedulingDeviceBo);
            List<String> deviceNos = appSchedulingDeviceVos.stream()
                .map(AppSchedulingDeviceVo::getDeviceNo)
                .filter(StringUtils::isNotBlank)
                .distinct()
                .toList();
            List<AppDeviceVo> queriedDevices = appDeviceService.queryByDeviceNos(deviceNos);
            Map<String, AppDeviceVo> devicesByNo = new HashMap<>();
            for (AppDeviceVo device : queriedDevices) {
                if (device != null && StringUtils.isNotBlank(device.getDeviceNo())) {
                    devicesByNo.putIfAbsent(device.getDeviceNo(), device);
                }
            }
            List<AppDeviceVo> deviceList = deviceNos.stream()
                .map(devicesByNo::get)
                .filter(Objects::nonNull)
                .toList();
            List<Map<String, Object>> detailList = new ArrayList<>();

            for (AppScheduleDetailVo detail:list){
                List<Object> timeSlots = new ArrayList<>();
                JSONArray mapArray = JSONUtil.parseArray(detail.getTimeData());
                for (Object timeSlot:mapArray){
                    timeSlots.add(timeSlot);
                }

                Map<String, Object> map = new HashMap<>();
                map.put("id",detail.getId() );
                map.put("weekday",detail.getWeekday() );
                map.put("timeData",timeSlots );
                map.put("triggerType",detail.getTriggerType() );
                map.put("status",detail.getStatus() );
                detailList.add(map);
            }



            appScheduleVo.setDetails(detailList);
            appScheduleVo.setDeviceNos(deviceList);
            return R.ok(appScheduleVo);
        }

        return R.fail(appScheduleVo);
    }


    /**
     * 修改排程状态
     * @param
     * @return
     */
    @PutMapping("/editScheduleStatus")
    public R<Void> editScheduleStatus(@NotNull(message = "主键不能为空")
                                            @RequestBody AppScheduleBo bo) {
        assertScheduleOwned(bo.getId());
        bo.setUserId(LoginHelper.getUserId());
        Boolean updated = appScheduleService.updateStatusByBo(bo);
        if (updated) {
            // 向所有绑定设备下发排程状态变更
            notifyBoundDevicesScheduleStatusChange(bo.getId(), bo.getStatus());
        }
        return toAjax(updated);
    }

    /**
     * 修改日程排列
     */
    @Log(title = "日程排列", businessType = BusinessType.UPDATE)
    @RepeatSubmit()
    @PutMapping("/updataschedule")
    public R<Object> editSchedule(@Validated(EditGroup.class) @RequestBody AppScheduleBo bo) {
        assertScheduleOwned(bo.getId());
        bo.setUserId(LoginHelper.getUserId());
        Map<String, Object> map = appScheduleService.updateByBo(bo);
        if (Boolean.valueOf(map.get("flag").toString())){
            // 向所有绑定设备下发排程更新
            notifyBoundDevicesScheduleUpdate(bo.getId());
            return R.ok(map.get("appSchedule"));
        }else {
            return R.fail();
        }
    }


    /**
     * 删除日程排列
     *
     * @param ids 主键串
     */
    @Log(title = "日程排列", businessType = BusinessType.DELETE)
    @DeleteMapping("/deleteschedule/{ids}")
    public R<Void> removeSchedule(@NotEmpty(message = "主键不能为空")
                          @PathVariable Long[] ids) {
        for (Long id : ids) {
            assertScheduleOwned(id);
        }
        Map<Long, List<String>> boundDeviceNosBySchedule = new HashMap<>();
        for (Long id : ids) {
            boundDeviceNosBySchedule.put(id, findBoundDeviceNos(id));
        }
        Boolean deleted = appScheduleService.deleteWithValidByIds(List.of(ids), true);
        if (deleted) {
            for (Map.Entry<Long, List<String>> entry : boundDeviceNosBySchedule.entrySet()) {
                notifyScheduleUnbind(entry.getKey(), entry.getValue());
            }
        }
        return toAjax(deleted);
    }

    /**
     * 历史
     * 查询浇水记录列表
     */
    @GetMapping("/waterLogList")
    public TableDataInfo<AppWateringLogVo> list(AppWateringLogBo bo, PageQuery pageQuery) {
        bo.setUserId(LoginHelper.getUserId());
        pageQuery.setOrderByColumn("createTime");
        pageQuery.setIsAsc("desc");
        TableDataInfo<AppWateringLogVo> appWateringLogVoTableDataInfo = appWateringLogService.queryPageList(bo, pageQuery);
        AppDeviceBo appDeviceBo = new AppDeviceBo();
        appDeviceBo.setUserId(LoginHelper.getUserId());
        List<AppDeviceVo> appDeviceVos = appDeviceService.queryList(appDeviceBo);
        List<AppWateringLogVo> wateringLogs = new ArrayList<>();
        for (AppWateringLogVo vo : appWateringLogVoTableDataInfo.getRows()){
            for(AppDeviceVo appDeviceVo:appDeviceVos){
                if (appDeviceVo.getDeviceNo().equals(vo.getDeviceNo())){
                    vo.setDeviceName(appDeviceVo.getDeviceName());
                    vo.setDeviceNo(appDeviceVo.getDeviceNo());
                }
            }
            wateringLogs.add(vo);
        }
        appWateringLogVoTableDataInfo.setRows(wateringLogs);
        return appWateringLogVoTableDataInfo;
    }

    /**
     * 查询设备最新进行中的浇水记录
     */
    @GetMapping("/latestWaterLog")
    public R<Map<String, Object>> latestWaterLog(@RequestParam String deviceNo) {
        assertDeviceOwned(deviceNo);

        AppWateringLogBo bo = new AppWateringLogBo();
        bo.setUserId(LoginHelper.getUserId());
        bo.setDeviceNo(deviceNo);
        bo.setStatus("1");
        List<AppWateringLogVo> logs = appWateringLogService.queryList(bo);
        if (logs == null || logs.isEmpty()) {
            return R.ok(null);
        }

        AppWateringLogVo latest = logs.stream()
            .max(Comparator.comparing(AppWateringLogVo::getId, Comparator.nullsFirst(Long::compareTo)))
            .orElse(null);
        if (latest == null) {
            return R.ok(null);
        }

        Map<String, Object> result = new HashMap<>();
        result.put("startTime", formatSecondTime(latest.getStartTime()));
        result.put("endTime", formatSecondTime(latest.getEndTime()));
        return R.ok(result);
    }


    /**
     * 删除浇水记录
     *
     * @param ids 主键串
     */
    @Log(title = "浇水记录", businessType = BusinessType.DELETE)
    @DeleteMapping("/waterLog/{ids}")
    public R<Void> remove(@NotEmpty(message = "主键不能为空")
                          @PathVariable Long[] ids) {
        for (Long id : ids) {
            assertWateringLogOwned(id);
        }
        return toAjax(appWateringLogService.deleteWithValidByIds(List.of(ids), true));
    }


    /**
     * 数据统计
     */
    @GetMapping("/dataStatistics")
    public R<Map<String, Long>> dataStatistics() {
        Map<String, Long> map = new HashMap<>();
        Long userId = LoginHelper.getUserId();
        //总浇水次数
        Long count = appWateringLogService.queryCount(userId);
        //本月浇水次数
        Long monCount = appWateringLogService.queryByMonCount(userId);
        //本周浇水次数
        Long weekCount = appWateringLogService.queryByWeekCount(userId);
        //排程浇水次数
        Long schedule = appWateringLogService.queryByTriggerTypeCount(userId, "0");
        //手动浇水次数
        Long manualNum = appWateringLogService.queryByTriggerTypeCount(userId, "1");
        //总浇水时间
        Long allTime = Optional.ofNullable(appWateringLogService.queryByTotalWateringTime(userId)).orElse(0L);
        //平均浇水时间
        Long averageTime = count == null || count == 0 ? 0L : allTime / count;

        map.put("count",count);
        map.put("monCount",monCount);
        map.put("weekCount",weekCount);
        map.put("schedule",schedule);
        map.put("manualNum",manualNum);
        map.put("allTime",allTime);
        map.put("averageTime",averageTime);
        return R.ok(map);

    }

    /**
     * 获取用户信息
     *
     * @return 用户信息
     */
    @GetMapping("/getUserInfo")
    public R<UserInfoVo> getUserInfo() {
        UserInfoVo userInfoVo = new UserInfoVo();
        LoginUser loginUser = LoginHelper.getLoginUser();
        SysUserVo user = DataPermissionHelper.ignore(() -> userService.selectUserById(loginUser.getUserId()));
        if (ObjectUtil.isNull(user)) {
            return R.fail(MessageUtils.message("app.user.data.denied"));
        }
        userInfoVo.setUser(user);
        userInfoVo.setPermissions(loginUser.getMenuPermission());
        userInfoVo.setRoles(loginUser.getRolePermission());
        return R.ok(userInfoVo);
    }

    /**
     * 上传图片到OSS对象存储
     */
    @PostMapping(value = "/uploadImage", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public R<SysOssUploadVo> uploadImage(@RequestPart("file") MultipartFile file) {
        validateImageFile(file);

        SysOssVo oss = ossService.upload(file);
        SysOssUploadVo uploadVo = new SysOssUploadVo();
        uploadVo.setUrl(oss.getUrl());
        uploadVo.setFileName(oss.getOriginalName());
        uploadVo.setOssId(oss.getOssId() == null ? null : oss.getOssId().toString());
        return R.ok(uploadVo);
    }

    private void validateImageFile(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new ServiceException("上传图片不能为空");
        }
        if (file.getSize() > MAX_IMAGE_SIZE_BYTES) {
            throw new ServiceException("图片大小不能超过20MB");
        }

        String contentType = StringUtils.lowerCase(file.getContentType());
        if (StringUtils.isBlank(contentType) || !contentType.startsWith("image/")) {
            throw new ServiceException("只能上传图片文件");
        }

        String extension = StringUtils.lowerCase(FileUtil.extName(file.getOriginalFilename()));
        if (!ALLOWED_IMAGE_EXTENSIONS.contains(extension)) {
            throw new ServiceException("仅支持jpg、jpeg、png、gif、webp、bmp图片");
        }

        String normalizedExtension = normalizeImageType(extension);
        if (!IMAGE_CONTENT_TYPES.get(normalizedExtension).contains(contentType)) {
            throw new ServiceException("图片文件类型不匹配");
        }

        try (InputStream inputStream = file.getInputStream()) {
            String detectedType = normalizeImageType(FileTypeUtil.getType(inputStream));
            if (!normalizedExtension.equals(detectedType)) {
                throw new ServiceException("图片文件内容不合法");
            }
        } catch (IOException e) {
            throw new ServiceException("读取图片文件失败");
        }
    }

    private String normalizeImageType(String imageType) {
        String normalizedType = StringUtils.lowerCase(imageType);
        return "jpeg".equals(normalizedType) ? "jpg" : normalizedType;
    }

    /**
     * 检查APP版本更新
     */
    @GetMapping("/checkVersion")
    public R<AppVersionCheckVo> checkVersion(@RequestParam(required = false, defaultValue = "android") String platform,
                                             @RequestParam(required = false) String currentVersion) {
        if (StringUtils.isBlank(currentVersion)) {
            throw new ServiceException("当前版本不能为空");
        }
        String normalizedPlatform = StringUtils.lowerCase(StringUtils.trim(platform));
        if (!StringUtils.equalsAny(normalizedPlatform, "android", "ios")) {
            throw new ServiceException("平台类型仅支持 android 或 ios");
        }

        AppVersionVo version = appVersionService.queryLatestByPlatform(normalizedPlatform);
        if (version == null || StringUtils.isBlank(version.getLatestVersion())) {
            throw new ServiceException("版本配置不存在");
        }

        AppVersionCheckVo result = new AppVersionCheckVo();
        result.setPlatform(normalizedPlatform);
        result.setCurrentVersion(currentVersion);
        result.setLatestVersion(version.getLatestVersion());
        result.setVersionCode(version.getVersionCode());
        result.setUpdateAvailable(!StringUtils.equals(currentVersion, version.getLatestVersion()));
        result.setForceUpdate(StringUtils.equals(version.getForceUpdate(), "1"));
        result.setDownloadUrl(version.getDownloadUrl());
        result.setReleaseNotes(version.getReleaseNotes());
        return R.ok(result);
    }

    /**
     * 修改用户
     */
    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
    @RepeatSubmit()
    @PutMapping("/updataUser")
    public R<Void> editUser( @RequestBody SysUserBo user) {
        userService.checkUserAllowed(user.getUserId());
        userService.checkUserDataScope(user.getUserId());
        if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) {
            return R.fail(MessageUtils.message("app.user.phone.exists", user.getUserName()));
        }

        return toAjax(userService.updateAppUser(user));
    }

    /**
     * 修改登录密码
     */
    @RepeatSubmit()
    @PutMapping("/updataPassword")
    public R<Void> updataPassword(@RequestBody  SysUserVo vo) {
        Long userId = LoginHelper.getUserId();
        SysUserVo sysUserVo = userService.selectUserById(userId);
        if (!BCrypt.checkpw(vo.getOldPassword(), sysUserVo.getPassword())){
            throw new UserException("user.password.not.match");
        }
        SysUserBo userBo = new SysUserBo();
        userBo.setUserId(userId);
        userBo.setPassword(vo.getPassword());
        return toAjax(userService.updateUserPas(userBo));
    }

    /**
     * app用户手机号/邮箱找回忘记密码
     */
    @ApiEncrypt
    @PostMapping("/retrievePassword")
    public R<Void> forgot(@RequestBody String body) {
        SysUserBo bo = JsonUtils.parseObject(body, SysUserBo.class);
        bo.setUserId(LoginHelper.getUserId());

        if(Validator.isEmail(bo.getUserName())){
            boolean checkPhoneFlag = userService.checkEmailUnique(bo);
            if (!checkPhoneFlag){
                throw new UserException("user.email.not.username");
            }
        }else if (Validator.isMobile(bo.getUserName())){
            boolean checkPhoneFlag = userService.checkPhoneUnique(bo);
            if (!checkPhoneFlag){
                throw new UserException("user.mobile.phone.number.not.username");
            }
        }

        return toAjax(userService.updateUserPas(bo));
    }

    private String formatSecondTime(Date time) {
        return time == null ? null : time.toInstant()
            .atZone(ZoneId.systemDefault())
            .toLocalDateTime()
            .format(SECOND_TIME_FORMATTER);
    }

    private AppDeviceVo getOwnedDevice(String deviceNo) {
        if (StringUtils.isBlank(deviceNo)) {
            throw new ServiceException("app.device.no.not.blank");
        }
        AppDeviceVo device = appDeviceService.queryById(deviceNo);
        Long userId = LoginHelper.getUserId();
        if (device == null || !Objects.equals(device.getUserId(), userId)) {
            throw new ServiceException("app.device.not.exists.or.denied");
        }
        return device;
    }

    private void assertDeviceOwned(String deviceNo) {
        getOwnedDevice(deviceNo);
    }

    private AppScheduleVo getOwnedSchedule(Long scheduleId) {
        if (scheduleId == null) {
            throw new ServiceException("app.schedule.id.not.blank");
        }
        AppScheduleVo schedule = appScheduleService.queryById(scheduleId);
        Long userId = LoginHelper.getUserId();
        if (schedule == null || !Objects.equals(schedule.getUserId(), userId)) {
            throw new ServiceException("app.schedule.not.exists.or.denied");
        }
        return schedule;
    }

    private void assertScheduleOwned(Long scheduleId) {
        getOwnedSchedule(scheduleId);
    }

    private void assertWateringLogOwned(Long id) {
        if (id == null) {
            throw new ServiceException("app.watering.log.id.not.blank");
        }
        AppWateringLogVo log = appWateringLogService.queryById(id);
        Long userId = LoginHelper.getUserId();
        if (log == null || !Objects.equals(log.getUserId(), userId)) {
            throw new ServiceException("app.watering.log.not.exists.or.denied");
        }
    }

    /**
     * 查找排程关联的所有设备编号
     */
    private List<String> findBoundDeviceNos(Long scheduleId) {
        AppSchedulingDeviceBo bo = new AppSchedulingDeviceBo();
        bo.setScheduleId(scheduleId);
        List<AppSchedulingDeviceVo> bindings = appSchedulingDeviceService.queryList(bo);
        return bindings.stream()
            .map(AppSchedulingDeviceVo::getDeviceNo)
            .filter(StringUtils::isNotBlank)
            .toList();
    }

    /**
     * 向排程关联的所有设备重新下发排程信息（用于排程内容/状态变更）
     */
    private void notifyBoundDevicesScheduleUpdate(Long scheduleId) {
        List<String> deviceNos = findBoundDeviceNos(scheduleId);
        List<Map<String, Object>> scheduleDetails = loadScheduleDetailPayload(scheduleId);
        for (String deviceNo : deviceNos) {
            try {
                deviceCommandService.sendScheduleBindCommand(deviceNo,
                    buildScheduleBindPayload(deviceNo, scheduleDetails));
            } catch (Exception e) {
                log.warn("[排程通知] 下发排程更新失败 排程ID={} 设备编号={} 原因={}",
                    scheduleId, deviceNo, e.getMessage(), e);
            }
        }
    }

    /**
     * 按排程开关状态下发不同主题：关闭走取消主题，开启重新下发排程。
     */
    private void notifyBoundDevicesScheduleStatusChange(Long scheduleId, String status) {
        List<String> deviceNos = findBoundDeviceNos(scheduleId);
        List<Map<String, Object>> scheduleDetails = "0".equals(status)
            ? Collections.emptyList()
            : loadScheduleDetailPayload(scheduleId);
        for (String deviceNo : deviceNos) {
            try {
                if ("0".equals(status)) {
                    deviceCommandService.sendScheduleCanceledCommand(deviceNo, scheduleId);
                } else {
                    deviceCommandService.sendScheduleBindCommand(deviceNo,
                        buildScheduleBindPayload(deviceNo, scheduleDetails));
                }
            } catch (Exception e) {
                log.warn("[排程通知] 下发排程状态变更失败 排程ID={} 状态={} 设备编号={} 原因={}",
                    scheduleId, status, deviceNo, e.getMessage(), e);
            }
        }
    }

    /**
     * 向排程关联的所有设备下发排程解绑通知
     */
    private void notifyBoundDevicesScheduleUnbind(Long scheduleId) {
        notifyScheduleUnbind(scheduleId, findBoundDeviceNos(scheduleId));
    }

    private void notifyScheduleUnbind(Long scheduleId, List<String> deviceNos) {
        for (String deviceNo : deviceNos) {
            try {
                deviceCommandService.sendScheduleUnbindCommand(deviceNo, scheduleId);
            } catch (Exception e) {
                log.warn("[排程通知] 下发排程解绑失败 排程ID={} 设备编号={} 原因={}",
                    scheduleId, deviceNo, e.getMessage(), e);
            }
        }
    }

}
