package org.dromara.mqtt;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.domain.mqtt.DeviceCommand;
import org.dromara.app.domain.mqtt.DeviceCommandAck;
import org.dromara.app.mapper.AppDeviceMapper;
import org.dromara.app.service.IDeviceCommandAckHandler;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.mqtt.config.properties.MqttProperties;
import org.redisson.api.RLock;
import org.redisson.api.RSet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;

@Slf4j
@Service
@RequiredArgsConstructor
public class MqttCommandAckService implements IDeviceCommandAckHandler {

    // 字段注入 + @Lazy 打破 MqttClientManager → MqttMessageDispatcher → MqttCommandAckService → MqttClientManager 循环
    // publish() 仅在 retryExpiredCommands() 运行时调用，构造阶段无需立即解析
    // 注意：不能声明为 final，否则 Lombok @RequiredArgsConstructor 会放入构造器，@Lazy 失效
    @Lazy
    @Autowired
    private MqttClientManager mqttClientManager;
    private final MqttProperties mqttProperties;
    private final AppDeviceMapper appDeviceMapper;
    private volatile boolean startupCleanupReady;
    private static final String DEVICE_STATUS_LOCK_PREFIX = "lock:mqtt:device:status:";

    static boolean ackMatchesPendingCommand(String topicDeviceNo, DeviceCommand pendingCommand) {
        return StringUtils.isNotBlank(topicDeviceNo)
            && pendingCommand != null
            && topicDeviceNo.equals(pendingCommand.getDeviceNo());
    }

    static boolean resolveMissingCommandId(DeviceCommandAck ack, List<DeviceCommand> pendingCommands) {
        if (ack == null || StringUtils.isNotBlank(ack.getCommandId())) {
            return ack != null;
        }
        if (pendingCommands == null || pendingCommands.size() != 1) {
            return false;
        }
        DeviceCommand command = pendingCommands.get(0);
        if (command == null || StringUtils.isBlank(command.getCommandId())) {
            return false;
        }
        ack.setCommandId(command.getCommandId());
        if (StringUtils.isBlank(ack.getStatus())) {
            ack.setStatus("1");
        }
        return true;
    }

    @Override
    public void handleAck(String deviceNo, String payload) {
        if (StringUtils.isBlank(payload)) {
            log.warn("[MQTT] 收到空 ACK 设备编号={}", deviceNo);
            return;
        }

        String trimmedPayload = payload.trim();
        if (!trimmedPayload.startsWith("{")) {
            handlePlainAck(deviceNo, trimmedPayload);
            return;
        }

        DeviceCommandAck ack;
        try {
            ack = JsonUtils.parseObject(trimmedPayload, DeviceCommandAck.class);
        } catch (RuntimeException e) {
            log.warn("[MQTT] ACK JSON 格式错误 设备编号={} 消息体={}", deviceNo, payload, e);
            return;
        }
        if (ack == null) {
            log.warn("[MQTT] ACK 缺少命令编号 设备编号={} 消息体={}", deviceNo, payload);
            return;
        }
        ack.setDeviceNo(deviceNo);
        if (StringUtils.isBlank(ack.getCommandId())) {
            List<DeviceCommand> pendingCommands = findPendingCommandsByDeviceNo(deviceNo);
            if (!resolveMissingCommandId(ack, pendingCommands)) {
                log.warn("[MQTT] ACK 缺少命令编号且无法唯一匹配待确认命令 设备编号={} 待确认数量={} 消息体={}",
                    deviceNo, pendingCommands.size(), payload);
                return;
            }
        }
        long lockWaitMs = mqttProperties.getCommandAck().getAckLockWaitMs();
        CommandLockResult ackResult = withCommandLock(ack.getCommandId(), lockWaitMs, () -> {
            DeviceCommand pending = RedisUtils.getCacheObject(pendingKey(ack.getCommandId()));
            if (pending == null) {
                log.warn("[MQTT] ACK 对应命令不存在或已过期 设备编号={} 命令编号={}", deviceNo, ack.getCommandId());
                return false;
            }
            if (!ackMatchesPendingCommand(deviceNo, pending)) {
                log.warn("[MQTT] ACK 设备编号与待确认命令不匹配，已拒绝 设备编号={} 命令编号={} 命令设备={}",
                    deviceNo, ack.getCommandId(), pending.getDeviceNo());
                return false;
            }
            deletePending(ack.getCommandId());
            RedisUtils.setCacheObject(ackKey(ack.getCommandId()), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds()));
            return true;
        });
        if (ackResult == CommandLockResult.LOCK_UNAVAILABLE) {
            log.warn("[MQTT] ACK 等待命令锁超时，本次跳过 设备编号={} 命令编号={} 等待毫秒={}",
                deviceNo, ack.getCommandId(), lockWaitMs);
            return;
        }
        if (ackResult == CommandLockResult.INTERRUPTED) {
            return;
        }
        if (ackResult == CommandLockResult.ACTION_FAILED) {
            return;
        }
        refreshDeviceOnline(deviceNo);
        log.info("[MQTT] 收到命令确认 设备编号={} 命令编号={} message={}", deviceNo, ack.getCommandId(), ack.getMessage());
    }

    public void savePending(DeviceCommand command) {
        RedisUtils.setCacheObject(pendingKey(command.getCommandId()), command, Duration.ofSeconds(mqttProperties.getCommandAck().getPendingTtlSeconds()));
        pendingIds().add(command.getCommandId());
    }

    public int clearPendingCommands() {
        RSet<String> pendingIds = pendingIds();
        Set<String> commandIds = pendingIds.readAll();
        pendingIds.removeAll(commandIds);
        startupCleanupReady = true;

        RuntimeException cleanupFailure = null;
        for (String commandId : commandIds) {
            try {
                RedisUtils.deleteObject(pendingKey(commandId));
            } catch (RuntimeException e) {
                if (cleanupFailure == null) {
                    cleanupFailure = new RuntimeException("Failed to delete MQTT pending command object: " + commandId, e);
                } else {
                    cleanupFailure.addSuppressed(e);
                }
            }
        }
        if (cleanupFailure != null) {
            throw cleanupFailure;
        }
        return commandIds.size();
    }

    boolean isStartupCleanupReady() {
        return startupCleanupReady;
    }

    public void removePending(String commandId) {
        if (StringUtils.isBlank(commandId)) {
            return;
        }
        withCommandLock(commandId, () -> {
            deletePending(commandId);
            return true;
        });
    }

    private void handlePlainAck(String deviceNo, String payload) {
        List<DeviceCommand> pendingCommands = findPendingCommandsByDeviceNo(deviceNo);
        refreshDeviceOnline(deviceNo);
        if (pendingCommands.size() != 1) {
            log.warn("[MQTT] 非 JSON ACK 无法唯一匹配待确认命令 设备编号={} 待确认数量={} 消息体={}", deviceNo, pendingCommands.size(), payload);
            return;
        }

        DeviceCommand command = pendingCommands.get(0);
        String commandId = command.getCommandId();
        DeviceCommandAck ack = new DeviceCommandAck();
        ack.setDeviceNo(deviceNo);
        ack.setCommandId(commandId);
        ack.setStatus("1");
        ack.setMessage(payload);

        long lockWaitMs = mqttProperties.getCommandAck().getAckLockWaitMs();
        CommandLockResult ackResult = withCommandLock(commandId, lockWaitMs, () -> {
            DeviceCommand pending = RedisUtils.getCacheObject(pendingKey(commandId));
            if (pending == null) {
                log.warn("[MQTT] 非 JSON ACK 对应命令不存在或已过期 设备编号={} 命令编号={}", deviceNo, commandId);
                return false;
            }
            if (!ackMatchesPendingCommand(deviceNo, pending)) {
                log.warn("[MQTT] 非 JSON ACK 设备编号与待确认命令不匹配，已拒绝 设备编号={} 命令编号={} 命令设备={}",
                    deviceNo, commandId, pending.getDeviceNo());
                return false;
            }
            deletePending(commandId);
            RedisUtils.setCacheObject(ackKey(commandId), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds()));
            return true;
        });
        if (ackResult == CommandLockResult.LOCK_UNAVAILABLE) {
            log.warn("[MQTT] 非 JSON ACK 等待命令锁超时，本次跳过 设备编号={} 命令编号={} 等待毫秒={}",
                deviceNo, commandId, lockWaitMs);
            return;
        }
        if (ackResult == CommandLockResult.INTERRUPTED) {
            return;
        }
        if (ackResult == CommandLockResult.ACTION_FAILED) {
            return;
        }
        log.info("[MQTT] 收到非 JSON 命令确认 设备编号={} 命令编号={} 消息体={}", deviceNo, commandId, payload);
    }

    private CommandLockResult withCommandLock(String commandId, BooleanSupplier action) {
        return withCommandLock(commandId, 0L, action);
    }

    private List<DeviceCommand> findPendingCommandsByDeviceNo(String deviceNo) {
        List<DeviceCommand> commands = new ArrayList<>();
        for (String commandId : pendingIds().readAll()) {
            DeviceCommand command = RedisUtils.getCacheObject(pendingKey(commandId));
            if (command != null && deviceNo.equals(command.getDeviceNo())) {
                commands.add(command);
            }
        }
        return commands;
    }

    public void retryExpiredCommands() {
        long now = System.currentTimeMillis();
        Collection<String> commandIds = pendingIds().readAll();
        for (String commandId : commandIds) {
            retryCommandIfLocked(commandId, now);
        }
    }

    private void retryCommandIfLocked(String commandId, long now) {
        withCommandLock(commandId, () -> {
            retryCommand(commandId, now);
            return true;
        });
    }

    private void retryCommand(String commandId, long now) {
        DeviceCommand command = RedisUtils.getCacheObject(pendingKey(commandId));
        if (command == null) {
            pendingIds().remove(commandId);
            return;
        }
        if (command.getNextRetryAt() > now) {
            return;
        }
        if (command.getRetryCount() >= mqttProperties.getCommandAck().getMaxRetryCount()) {
            deletePending(commandId);
            log.warn("[MQTT] 命令重试次数已达上限 设备编号={} 命令编号={}", command.getDeviceNo(), commandId);
            return;
        }
        if (!isDeviceOnline(command.getDeviceNo())) {
            command.setNextRetryAt(now + mqttProperties.getCommandAck().getRetryIntervalMs());
            savePending(command);
            log.debug("[MQTT] 设备离线，命令延后重试 设备编号={} 命令编号={}", command.getDeviceNo(), commandId);
            return;
        }

        command.setRetryCount(command.getRetryCount() + 1);
        command.setLastSentAt(now);
        command.setNextRetryAt(now + mqttProperties.getCommandAck().getRetryIntervalMs());
        try {
            mqttClientManager.publish(command.getTopic(), JsonUtils.toJsonString(command.getPayload()));
            savePending(command);
            log.info("[MQTT] 命令已重新下发 设备编号={} 命令编号={} 重试次数={}", command.getDeviceNo(), commandId, command.getRetryCount());
        } catch (RuntimeException e) {
            savePending(command);
            log.warn("[MQTT] 命令重新下发失败，已延后重试 设备编号={} 命令编号={} 重试次数={}",
                command.getDeviceNo(), commandId, command.getRetryCount(), e);
        }
    }

    private boolean isDeviceOnline(String deviceNo) {
        Map<String, Object> status = RedisUtils.getCacheObject(mqttProperties.getCommandAck().getDeviceStatusCachePrefix() + deviceNo);
        if (status == null) {
            return false;
        }
        Object value = status.get("status");
        return value == null || !"0".equals(String.valueOf(value));
    }

    private void refreshDeviceOnline(String deviceNo) {
        RLock lock = RedisUtils.getClient().getLock(DEVICE_STATUS_LOCK_PREFIX + deviceNo);
        boolean locked = false;
        try {
            locked = lock.tryLock(0, 10, TimeUnit.SECONDS);
            if (!locked) {
                // 其他线程正在刷新，本次跳过；状态写入幂等，下条 ACK 仍会兜底刷新
                return;
            }
            Map<String, Object> statusCache = new HashMap<>();
            statusCache.put("deviceNo", deviceNo);
            statusCache.put("status", "1");
            statusCache.put("lastReportTime", Instant.now().toString());
            RedisUtils.setCacheObject(
                mqttProperties.getCommandAck().getDeviceStatusCachePrefix() + deviceNo,
                statusCache,
                Duration.ofSeconds(mqttProperties.getCommandAck().getDeviceStatusCacheTtlSeconds())
            );
            appDeviceMapper.update(null,
                new LambdaUpdateWrapper<AppDevice>()
                    .set(AppDevice::getStatus, "1")
                    .eq(AppDevice::getDeviceNo, deviceNo)
            );
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.warn("[MQTT] 刷新设备在线状态被中断 设备编号={}", deviceNo);
        } finally {
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private RSet<String> pendingIds() {
        return RedisUtils.getClient().getSet(mqttProperties.getCommandAck().getPendingSetKey());
    }

    private void deletePending(String commandId) {
        RedisUtils.deleteObject(pendingKey(commandId));
        pendingIds().remove(commandId);
    }

    private CommandLockResult withCommandLock(String commandId, long waitTimeMs, BooleanSupplier action) {
        RLock lock = RedisUtils.getClient().getLock(mqttProperties.getCommandAck().getRetryLockKeyPrefix() + commandId);
        boolean locked = false;
        try {
            locked = lock.tryLock(waitTimeMs, mqttProperties.getCommandAck().getRetryLockTtlMs(), TimeUnit.MILLISECONDS);
            if (!locked) {
                return CommandLockResult.LOCK_UNAVAILABLE;
            }
            return action.getAsBoolean() ? CommandLockResult.SUCCESS : CommandLockResult.ACTION_FAILED;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.warn("[MQTT] 命令锁等待被中断 命令编号={}", commandId);
            return CommandLockResult.INTERRUPTED;
        } catch (RuntimeException e) {
            log.error("[MQTT] 命令锁内处理失败 命令编号={}", commandId, e);
            return CommandLockResult.ACTION_FAILED;
        } finally {
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    private enum CommandLockResult {
        SUCCESS,
        LOCK_UNAVAILABLE,
        INTERRUPTED,
        ACTION_FAILED
    }

    private String pendingKey(String commandId) {
        return mqttProperties.getCommandAck().getPendingKeyPrefix() + commandId;
    }

    private String ackKey(String commandId) {
        return mqttProperties.getCommandAck().getAckKeyPrefix() + commandId;
    }
}
