package org.dromara.app.service.impl;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.qrcode.QrCodeUtil;
import com.baomidou.lock.annotation.Lock4j;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.domain.AppWateringLog;
import org.dromara.app.domain.bo.AppDeviceBo;
import org.dromara.app.domain.vo.AppDeviceVo;
import org.dromara.app.domain.vo.DashboardStatsVo;
import org.dromara.app.mapper.AppDeviceMapper;
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
import org.dromara.app.mapper.AppWateringLogMapper;
import org.dromara.app.service.IAppDeviceService;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.app.service.IDeviceCommandService;
import org.dromara.common.core.enums.UserType;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.oss.entity.UploadResult;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.List;

@Slf4j
@RequiredArgsConstructor
@Service
public class AppDeviceServiceImpl implements IAppDeviceService {

    private static final int QR_CODE_SIZE = 300;
    private static final int QR_CODE_LABEL_HEIGHT = 20;
    private static final int QR_CODE_LABEL_PADDING = 16;
    private static final int QR_CODE_LABEL_FONT_SIZE = 20;
    private static final int QR_CODE_LABEL_OFFSET_Y = -10;

    private final AppDeviceMapper baseMapper;
    private final AppSchedulingDeviceMapper schedulingDeviceMapper;
    private final AppWateringLogMapper wateringLogMapper;

    // @Lazy 打破 AppDeviceServiceImpl ↔ DeviceCommandServiceImpl 双向构造器循环
    @Lazy
    @Autowired
    private IDeviceCommandService deviceCommandService;
    private final IAppWateringLogService wateringLogService;

    @Override
    public AppDeviceVo queryById(String deviceNo) {
        return baseMapper.selectVoById(deviceNo);
    }

    @Override
    public List<AppDeviceVo> queryByDeviceNos(Collection<String> deviceNos) {
        if (deviceNos == null || deviceNos.isEmpty()) {
            return List.of();
        }
        return baseMapper.selectVoByIds(deviceNos);
    }

    @Override
    public TableDataInfo<AppDeviceVo> queryPageList(AppDeviceBo bo, PageQuery pageQuery) {
        Page<AppDeviceVo> page = pageQuery.build();
        page.orders().replaceAll(this::qualifyDeviceOrderItem);
        Page<AppDeviceVo> result = baseMapper.selectDeviceVoPage(page, bo);
        return TableDataInfo.build(result);
    }

    @Override
    public List<AppDeviceVo> queryList(AppDeviceBo bo) {
        return baseMapper.selectDeviceVoList(bo);
    }

    private OrderItem qualifyDeviceOrderItem(OrderItem item) {
        String column = item.getColumn();
        if (StringUtils.isBlank(column) || column.contains(".")) {
            return item;
        }
        item.setColumn("d." + column);
        return item;
    }

    private LambdaQueryWrapper<AppDevice> buildQueryWrapper(AppDeviceBo bo) {
        Map<String, Object> params = bo.getParams();
        LambdaQueryWrapper<AppDevice> lqw = Wrappers.lambdaQuery();
        lqw.orderByAsc(AppDevice::getDeviceNo);
        lqw.eq(StringUtils.isNotBlank(bo.getDeviceNo()), AppDevice::getDeviceNo, bo.getDeviceNo());
        lqw.eq(bo.getUserId() != null, AppDevice::getUserId, bo.getUserId());
        lqw.like(StringUtils.isNotBlank(bo.getDeviceName()), AppDevice::getDeviceName, bo.getDeviceName());
        lqw.eq(StringUtils.isNotBlank(bo.getDeviceImg()), AppDevice::getDeviceImg, bo.getDeviceImg());
        lqw.eq(StringUtils.isNotBlank(bo.getQrcode()), AppDevice::getQrcode, bo.getQrcode());
        lqw.eq(StringUtils.isNotBlank(bo.getStatus()), AppDevice::getStatus, bo.getStatus());
        lqw.eq(StringUtils.isNotBlank(bo.getPowerLevel()), AppDevice::getPowerLevel, bo.getPowerLevel());
        lqw.eq(bo.getPowerLevelUpdatatime() != null, AppDevice::getPowerLevelUpdatatime, bo.getPowerLevelUpdatatime());
        lqw.like(StringUtils.isNotBlank(bo.getWifiName()), AppDevice::getWifiName, bo.getWifiName());
        lqw.eq(StringUtils.isNotBlank(bo.getWifiPassword()), AppDevice::getWifiPassword, bo.getWifiPassword());
        lqw.eq(StringUtils.isNotBlank(bo.getDeviceEm()), AppDevice::getDeviceEm, bo.getDeviceEm());
        lqw.eq(StringUtils.isNotBlank(bo.getDeviceSn()), AppDevice::getDeviceSn, bo.getDeviceSn());
        lqw.eq(StringUtils.isNotBlank(bo.getFwVer()), AppDevice::getFwVer, bo.getFwVer());
        lqw.eq(StringUtils.isNotBlank(bo.getMacAddress()), AppDevice::getMacAddress, bo.getMacAddress());
        lqw.eq(bo.getExpirationTime() != null, AppDevice::getExpirationTime, bo.getExpirationTime());
        return lqw;
    }

    @Override
    public Boolean insertByBo(AppDeviceBo bo) {
       // fillBindTokenHash(bo);
        AppDevice add = MapstructUtils.convert(bo, AppDevice.class);
        validEntityBeforeInsert(add);
        normalizeMacAddress(add);
        ensureMacAddressAvailable(add);
        return baseMapper.insert(add) > 0;
    }

    @Override
    public Boolean updateByBo(AppDeviceBo bo) {
       // fillBindTokenHash(bo);
        AppDevice update = MapstructUtils.convert(bo, AppDevice.class);
        validEntityBeforeUpdate(update);
        normalizeMacAddress(update);
        ensureMacAddressAvailable(update);
        return baseMapper.updateById(update) > 0;
    }

    @Override
    public Boolean registerByMqtt(AppDeviceBo bo) {
        if (StringUtils.isBlank(bo.getDeviceNo())) {
            throw new ServiceException("设备编号不能为空");
        }
        AppDevice exists = baseMapper.selectById(bo.getDeviceNo());

        AppDevice device = MapstructUtils.convert(bo, AppDevice.class);
        device.setPowerLevelUpdatatime(new Date());
      //  applyRegisterBindToken(device, bo, exists);
        if (StringUtils.isBlank(device.getStatus())) {
            device.setStatus(exists == null || StringUtils.isBlank(exists.getStatus()) ? "0" : exists.getStatus());
        }
        if (StringUtils.isBlank(device.getWorkStatus())) {
            device.setWorkStatus(exists == null || StringUtils.isBlank(exists.getWorkStatus()) ? "2" : exists.getWorkStatus());
        }

        if (exists == null) {
            try {
                return baseMapper.insert(device) > 0;
            } catch (org.springframework.dao.DuplicateKeyException ignored) {
                device.setDeviceName(null);
                return baseMapper.updateById(device) > 0;
            }
        }

        return baseMapper.updateById(device) > 0;
    }

    static byte[] renderQrCodeImage(String content, String deviceNo) {
        try {
            byte[] qrCodeBytes = QrCodeUtil.generatePng(content, QR_CODE_SIZE, QR_CODE_SIZE);
            BufferedImage qrCodeImage = ImageIO.read(new ByteArrayInputStream(qrCodeBytes));
            BufferedImage image = new BufferedImage(
                QR_CODE_SIZE,
                QR_CODE_SIZE + QR_CODE_LABEL_HEIGHT,
                BufferedImage.TYPE_INT_RGB
            );

            Graphics2D graphics = image.createGraphics();
            try {
                graphics.setColor(Color.WHITE);
                graphics.fillRect(0, 0, image.getWidth(), image.getHeight());
                graphics.drawImage(qrCodeImage, 0, 0, null);
                drawCenteredDeviceNo(graphics, deviceNo);
            } finally {
                graphics.dispose();
            }

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            ImageIO.write(image, "png", outputStream);
            return outputStream.toByteArray();
        } catch (IOException e) {
            throw new ServiceException("设备二维码生成失败");
        }
    }

    private static void drawCenteredDeviceNo(Graphics2D graphics, String deviceNo) {
        String text = StringUtils.blankToDefault(deviceNo, "");
        graphics.setColor(Color.BLACK);
        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        Font font = new Font(Font.SANS_SERIF, Font.BOLD, QR_CODE_LABEL_FONT_SIZE);
        graphics.setFont(font);

        FontMetrics metrics = graphics.getFontMetrics();
        int maxTextWidth = QR_CODE_SIZE - QR_CODE_LABEL_PADDING * 2;
        while (metrics.stringWidth(text) > maxTextWidth && font.getSize() > 12) {
            font = font.deriveFont((float) font.getSize() - 1);
            graphics.setFont(font);
            metrics = graphics.getFontMetrics();
        }

        int x = (QR_CODE_SIZE - metrics.stringWidth(text)) / 2;
        int y = QR_CODE_SIZE + (QR_CODE_LABEL_HEIGHT - metrics.getHeight()) / 2 + metrics.getAscent()
            + QR_CODE_LABEL_OFFSET_Y;
        graphics.drawString(text, Math.max(QR_CODE_LABEL_PADDING, x), y);
    }

    @Override
    public AppDeviceVo bindRegisteredDevice(AppDeviceBo bo) {
        if (StringUtils.isBlank(bo.getDeviceNo())) {
            throw new ServiceException("设备编号不能为空");
        }
        Long userId = LoginHelper.getUserId();
        AppDevice exists = baseMapper.selectById(bo.getDeviceNo());
        if (exists == null) {
            throw new ServiceException("设备未上线注册，请先完成配网");
        }
        if (exists.getUserId() != null && !exists.getUserId().equals(userId)) {
            throw new ServiceException("设备已被其他用户绑定");
        }
       // validBindToken(bo.getBindToken(), exists);

        String status = StringUtils.isBlank(bo.getStatus()) ? exists.getStatus() : bo.getStatus();
        String wifiName = StringUtils.isBlank(bo.getWifiName()) ? exists.getWifiName() : bo.getWifiName();
        String wifiPassword = StringUtils.isBlank(bo.getWifiPassword()) ? exists.getWifiPassword() : bo.getWifiPassword();
        String deviceName = StringUtils.isBlank(bo.getDeviceName()) ? exists.getDeviceName() : bo.getDeviceName();
        int rows = baseMapper.bindIfAvailable(exists.getDeviceNo(), userId, deviceName, status,"0",wifiName,wifiPassword);
        if (rows <= 0) {
            AppDevice current = baseMapper.selectById(bo.getDeviceNo());
            if (current == null) {
                throw new ServiceException("设备未上线注册，请先完成配网");
            }
            if (current.getUserId() != null && !current.getUserId().equals(userId)) {
                throw new ServiceException("设备已被其他用户绑定");
            }
            throw new ServiceException("设备绑定失败，请重试");
        }
        //下发设备已绑定状态
        deviceCommandService.sendBindDeviceCommand(exists.getDeviceNo());
        return baseMapper.selectVoById(exists.getDeviceNo());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @Lock4j(keys = {"'device:command:' + #deviceNo"}, acquireTimeout = 5000, expire = 30000)
    public Boolean switchDevice(String deviceNo, String workStatus, String startTime, Integer durationMin) {
        if (StringUtils.isBlank(deviceNo)) {
            throw new ServiceException("设备编号不能为空");
        }
        if (StringUtils.isBlank(workStatus)) {
            throw new ServiceException("设备工作状态不能为空");
        }
        if ("1".equals(workStatus) && (durationMin == null || durationMin <= 0)) {
            throw new ServiceException("持续时间必须大于0");
        }

        Long userId = LoginHelper.getUserId();
        int rows = baseMapper.updateWorkStatusByUser(deviceNo, userId, workStatus);
        if (rows <= 0) {
            throw new ServiceException("设备不存在或无权操作");
        }

        AppDeviceVo device = baseMapper.selectVoById(deviceNo);
        if (ObjectUtil.isNotNull(device) && StringUtils.isNotBlank(device.getDeviceNo())) {
            // 通过业务层统一下发命令（含浇水日志记录）
            String commandId = deviceCommandService.sendSwitchCommand(deviceNo, workStatus, startTime, durationMin);
            log.info("[设备] 开关命令已下发 设备编号={} 命令编号={}", deviceNo, commandId);
        }

        return true;
    }

    @Override
    public Map<String, Object> bindDeviceStatus(AppDeviceBo bo) {
        Map<String, Object> params = new HashMap<>();
        params.put("bindDeviceStatus", 200);
        params.put("bindDeviceStatusName", "");

        if (bo == null) {
            params.put("bindDeviceStatus", 300);
            params.put("bindDeviceStatusName", "设备信息不能为空");
            return params;
        }

        if (StringUtils.isAllBlank(bo.getDeviceNo(), bo.getMacAddress(), bo.getDeviceInitName())) {
            params.put("bindDeviceStatus", 301);
            params.put("bindDeviceStatusName", "设备编号、MAC地址或设备初始名称不能为空");
            return params;
        }
        params.put("bindDevice", null);
        Long userId = LoginHelper.getUserId();
        AppDevice exists = findDeviceForBindStatus(bo);

        if (exists == null) {
            params.put("bindDeviceStatus", 302);
            params.put("bindDeviceStatusName", "设备未入库注册，请先进行入库");
            return params;
        }
        params.put("bindDevice", exists);
        if (exists.getUserId() == null && "2".equals(exists.getWorkStatus()) && "0".equals(exists.getStatus())) {
            params.put("bindDeviceStatus", 305);
            params.put("bindDeviceStatusName", "设备未配网,请先去配置网络");
            return params;
        }
        if (exists.getUserId() != null && !exists.getUserId().equals(userId)) {
            params.put("bindDeviceStatus", 303);
            params.put("bindDeviceStatusName", "设备已被其他用户绑定");
            return params;
        }
        if (exists.getUserId() == null ) {
            params.put("bindDeviceStatus", 304);
            params.put("bindDeviceStatusName", "设备未绑定，请先绑定用户");
            return params;
        }


        return params;
    }

    private AppDevice findDeviceForBindStatus(AppDeviceBo bo) {
        AppDevice exists = null;
        if (StringUtils.isNotBlank(bo.getDeviceNo())) {
            exists = baseMapper.selectById(bo.getDeviceNo());
            if (exists != null) {
                return exists;
            }
        }
        if (StringUtils.isNotBlank(bo.getMacAddress())) {
            exists = baseMapper.selectByMac(bo.getMacAddress());
            if (exists != null) {
                return exists;
            }
        }
        if (StringUtils.isNotBlank(bo.getDeviceInitName())) {
            return baseMapper.selectByDeviceInitName(bo.getDeviceInitName());
        }
        return null;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean generateQrCode(Collection<String> deviceNos) {
        if (deviceNos == null || deviceNos.isEmpty()) {
            throw new ServiceException("设备编号不能为空");
        }
        List<AppDevice> devices = baseMapper.selectByIds(deviceNos);
        if (devices.size() != new HashSet<>(deviceNos).size()) {
            throw new ServiceException("存在无效的设备编号");
        }

        for (AppDevice device : devices) {
            byte[] imageBytes = renderQrCodeImage(buildQrCodeContent(device), device.getDeviceNo());
            UploadResult uploadResult = OssFactory.instance().uploadSuffix(imageBytes, ".png", "image/png");
            int rows = baseMapper.update(null, new LambdaUpdateWrapper<AppDevice>()
                .set(AppDevice::getQrcode, uploadResult.getUrl())
                .eq(AppDevice::getDeviceNo, device.getDeviceNo()));
            if (rows <= 0) {
                throw new ServiceException("设备二维码保存失败");
            }
        }
        return true;
    }

    private String buildQrCodeContent(AppDevice device) {
        if (StringUtils.isBlank(device.getMacAddress())) {
            throw new ServiceException("设备MAC地址不能为空，无法生成二维码");
        }
        Map<String, String> content = new LinkedHashMap<>();
        content.put("deviceNo", device.getDeviceNo());
        content.put("mac", device.getMacAddress());
        content.put("transport", "ble");
        content.put("name", device.getDeviceInitName());
        return JsonUtils.toJsonString(content);
    }


    private void validEntityBeforeInsert(AppDevice entity) {
        if (StringUtils.isBlank(entity.getMacAddress())) {
            throw new ServiceException("MAC地址不能为空");
        }
    }

    private void normalizeMacAddress(AppDevice entity) {
        if (StringUtils.isNotBlank(entity.getMacAddress())) {
            entity.setMacAddress(entity.getMacAddress().toLowerCase(Locale.ROOT));
        }
    }

    private void ensureMacAddressAvailable(AppDevice entity) {
        if (StringUtils.isBlank(entity.getMacAddress())) {
            return;
        }
        AppDevice exists = baseMapper.selectByMac(entity.getMacAddress());
        if (exists != null && !Objects.equals(exists.getDeviceNo(), entity.getDeviceNo())) {
            throw new ServiceException("MAC地址已存在，不能重复绑定设备");
        }
    }

    private void validEntityBeforeUpdate(AppDevice entity) {
        if (StringUtils.isBlank(entity.getDeviceNo())) {
            throw new ServiceException("设备编号不能为空");
        }
    }

    @Override
    @Transactional
    public Boolean deleteWithValidByIds(Collection<String> deviceNos, Boolean isValid) {
        if (deviceNos == null || deviceNos.isEmpty()) {
            throw new ServiceException("设备编号不能为空");
        }

        List<String> distinctDeviceNos = deviceNos.stream()
            .filter(StringUtils::isNotBlank)
            .distinct()
            .toList();
        if (distinctDeviceNos.isEmpty()) {
            throw new ServiceException("设备编号不能为空");
        }

        boolean platformAdmin = isPlatformAdmin();
        Long userId = platformAdmin ? null : LoginHelper.getUserId();
        if (!platformAdmin) {
            if (userId == null) {
                throw new ServiceException("用户未登录");
            }

            Long ownedCount = baseMapper.selectCount(
                Wrappers.<AppDevice>lambdaQuery()
                    .in(AppDevice::getDeviceNo, distinctDeviceNos)
                    .eq(AppDevice::getUserId, userId)
            );
            if (ownedCount == null || ownedCount.intValue() != distinctDeviceNos.size()) {
                throw new ServiceException("设备不存在或无权操作");
            }
        }

        boolean flag = baseMapper.delete(
            Wrappers.<AppDevice>lambdaUpdate()
                .in(AppDevice::getDeviceNo, distinctDeviceNos)
                .eq(!platformAdmin, AppDevice::getUserId, userId)
        ) > 0;
        if (flag) {
            schedulingDeviceMapper.delete(
                new QueryWrapper<AppSchedulingDevice>().in("device_no", distinctDeviceNos)
            );
            wateringLogMapper.delete(new QueryWrapper<AppWateringLog>().in("device_no", distinctDeviceNos));
        }
        return flag;
    }

    private boolean isPlatformAdmin() {
        if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
            return true;
        }
        try {
            return UserType.SYS_USER == LoginHelper.getUserType();
        } catch (RuntimeException ignored) {
            return false;
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean unbindDevices(Collection<String> deviceNos, Long userId) {
        if (deviceNos == null || deviceNos.isEmpty()) {
            throw new ServiceException("设备编号不能为空");
        }
        if (userId == null) {
            throw new ServiceException("用户不能为空");
        }

        List<String> distinctDeviceNos = deviceNos.stream()
            .filter(StringUtils::isNotBlank)
            .distinct()
            .toList();
        if (distinctDeviceNos.isEmpty()) {
            throw new ServiceException("设备编号不能为空");
        }

        List<AppDevice> devices = baseMapper.selectList(
            Wrappers.<AppDevice>lambdaQuery()
                .in(AppDevice::getDeviceNo, distinctDeviceNos)
                .eq(AppDevice::getUserId, userId)
        );
        if (devices.size() != distinctDeviceNos.size()) {
            throw new ServiceException("设备不存在或无权操作");
        }

        for (AppDevice device : devices) {
            deviceCommandService.sendInitDeviceCommand(device.getDeviceNo());
        }

        int rows = baseMapper.update(null,
            Wrappers.<AppDevice>lambdaUpdate()
                .set(AppDevice::getUserId, null)
                .set(AppDevice::getWorkStatus, "0")
                .set(AppDevice::getWifiName, null)
                .set(AppDevice::getWifiPassword, null)
                .in(AppDevice::getDeviceNo, distinctDeviceNos)
                .eq(AppDevice::getUserId, userId)
        );
        if (rows != distinctDeviceNos.size()) {
            throw new ServiceException("设备解绑失败，请重试");
        }

        schedulingDeviceMapper.delete(
            new QueryWrapper<AppSchedulingDevice>().in("device_no", distinctDeviceNos)
        );
        wateringLogMapper.delete(
            new QueryWrapper<AppWateringLog>().in("device_no", distinctDeviceNos)
        );
        return true;
    }

    /**
     * 仪表盘统计 — 单条 SQL 聚合查询，避免 N+1 问题。
     */
    @Override
    public DashboardStatsVo getDashboardStats() {
        return baseMapper.selectDashboardStats();
    }
}
