package org.dromara.mqtt;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.mqtt.DeviceCommand;
import org.dromara.app.domain.mqtt.DeviceCommandAck;
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.ArrayList;
import java.util.HashMap;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@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;

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

    @Override
    public void handleAck(String deviceNo, String payload) {
        if (StringUtils.isBlank(payload)) {
            log.warn("[MQTT] 收到空 ACK deviceNo={}", 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={}", deviceNo, payload, e);
            return;
        }
        if (ack == null || ack.getCommandId() == null) {
            log.warn("[MQTT] ACK 缺少 commandId deviceNo={} payload={}", deviceNo, payload);
            return;
        }
        ack.setDeviceNo(deviceNo);
        RedisUtils.deleteObject(pendingKey(ack.getCommandId()));
        pendingIds().remove(ack.getCommandId());
        RedisUtils.setCacheObject(ackKey(ack.getCommandId()), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds()));
        refreshDeviceOnline(deviceNo);
        log.info("[MQTT] command ack received deviceNo={} commandId={} status={}", deviceNo, ack.getCommandId(), ack.getStatus());
    }

    private void handlePlainAck(String deviceNo, String payload) {
        List<DeviceCommand> pendingCommands = findPendingCommandsByDeviceNo(deviceNo);
        refreshDeviceOnline(deviceNo);
        if (pendingCommands.size() != 1) {
            log.warn("[MQTT] 非 JSON ACK 无法唯一匹配待确认命令 deviceNo={} pendingCount={} payload={}", 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);

        RedisUtils.deleteObject(pendingKey(commandId));
        pendingIds().remove(commandId);
        RedisUtils.setCacheObject(ackKey(commandId), ack, Duration.ofSeconds(mqttProperties.getCommandAck().getAckTtlSeconds()));
        log.info("[MQTT] plain command ack received deviceNo={} commandId={} payload={}", deviceNo, commandId, payload);
    }

    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) {
        RLock lock = RedisUtils.getClient().getLock(mqttProperties.getCommandAck().getRetryLockKeyPrefix() + commandId);
        boolean locked = false;
        try {
            locked = lock.tryLock(0, mqttProperties.getCommandAck().getRetryLockTtlMs(), TimeUnit.MILLISECONDS);
            if (!locked) {
                return;
            }
            retryCommand(commandId, now);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            log.warn("[MQTT] 命令重试锁等待被中断 commandId={}", commandId);
        } finally {
            if (locked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    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()) {
            RedisUtils.deleteObject(pendingKey(commandId));
            pendingIds().remove(commandId);
            log.warn("[MQTT] 命令重试次数已达上限 deviceNo={} commandId={}", command.getDeviceNo(), commandId);
            return;
        }
        if (!isDeviceOnline(command.getDeviceNo())) {
            command.setNextRetryAt(now + mqttProperties.getCommandAck().getRetryIntervalMs());
            savePending(command);
            log.debug("[MQTT] command retry delayed, device offline deviceNo={} commandId={}", command.getDeviceNo(), commandId);
            return;
        }

        command.setRetryCount(command.getRetryCount() + 1);
        command.setLastSentAt(now);
        command.setNextRetryAt(now + mqttProperties.getCommandAck().getRetryIntervalMs());
        mqttClientManager.publish(command.getTopic(), JsonUtils.toJsonString(command.getPayload()));
        savePending(command);
        log.info("[MQTT] command resent deviceNo={} commandId={} retry={}", command.getDeviceNo(), commandId, command.getRetryCount());
    }

    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) {
        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())
        );
    }

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

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

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