package com.hw.mqtt.auth;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.hw.mqtt.domain.DeviceInfoDto;
import com.hw.mqtt.enums.RedisKeys;
import com.ruoyi.common.core.utils.StringUtils;
import net.dreamlu.iot.mqtt.core.server.auth.IMqttServerAuthHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.tio.core.ChannelContext;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * mqtt tcp、websocket 认证
 * @author WenJY
 * @date 2023-03-14 12:19
 * @return null
 */
@Configuration(proxyBeanMethods = false)
public class MqttAuthHandler implements IMqttServerAuthHandler {

	private final Logger logger = LoggerFactory.getLogger(MqttAuthHandler.class);

	/**
	 * 存储设备信息，设备接入校验
	 */
	private List<DeviceInfoDto> deviceInfoDtos;

	@Value("${broker.whitelist}")
	private String clientIdWhitelist;

	private final StringRedisTemplate redisTemplate;

	public MqttAuthHandler(List<DeviceInfoDto> deviceInfoDtos, StringRedisTemplate redisTemplate) {
		this.deviceInfoDtos = deviceInfoDtos;
		this.redisTemplate = redisTemplate;
		this.InitDeviceInfoListByRedis();
	}

	/**
	 * 通过Redis初始化本地设备信息集合，只在该类初始化时执行
	 */
	private void InitDeviceInfoListByRedis(){
		try{
			String jsonStr = redisTemplate.opsForValue().get(RedisKeys.CLIENT_DEVICE_INFO.getKey());
			if(StringUtils.isEmpty(jsonStr)){
				logger.warn("通过Redis获取设备信息为空");
				return;
			}
			this.deviceInfoDtos = JSONArray.parseArray(jsonStr, DeviceInfoDto.class);
		}catch (Exception ex){
			logger.error("通过Redis获取设备信息方法处理异常："+ex.getMessage());
		}
	}

	/**
	 * 客户端认证逻辑实现
	 * @param context  ChannelContext
	 * @param uniqueId mqtt 内唯一id，默认和 clientId 相同
	 * @param clientId 客户端 ID
	 * @param userName 用户名
	 * @param password 密码
	 * @return
	 */
	@Override
	public boolean authenticate(ChannelContext context, String uniqueId, String clientId, String userName, String password) {
		//
		try{
			//白名单过滤
			if(clientIdWhitelist.contains(clientId)){
				return true;
			}else if (deviceInfoDtos!=null){
				Optional<DeviceInfoDto> optionalDeviceInfoDto = deviceInfoDtos.stream().distinct().filter(x -> x.getDeviceCode().equals(clientId)).findFirst();
				//判断本地集合中是否包含该设备信息，如果不包含再次读取Redis并初始化本地集合
				if (optionalDeviceInfoDto.isPresent()) {
					DeviceInfoDto deviceInfo = optionalDeviceInfoDto.get();
					return checkDeviceInfo(deviceInfo,clientId,userName,password);
				}
			}else {
				return NoDeviceInfoEvent(clientId,userName,password);
			}
		}catch (Exception ex){
			logger.error("客户端认证逻辑处理异常："+ex.getMessage());
		}
		return false;
	}

	/**
	 * 通过本地集合未获取到设备信息时重新获取进行校验
	 * @param clientId
	 * @param userName
	 * @param password
	 * @return
	 */
	private boolean NoDeviceInfoEvent(String clientId,String userName,String password){
		DeviceInfoDto deviceInfo = GetDeviceInfoByRedis(clientId);
		if (deviceInfo != null) {
			return checkDeviceInfo(deviceInfo,clientId,userName,password);
		}else {
			logger.warn("未获取到接入客户端的设备信息，禁止该设备接入");
			return false;
		}
	}

	/**
	 * 校验设备接入信息
	 * @param deviceInfo
	 * @param clientId
	 * @param userName
	 * @param passwd
	 * @return
	 */
	private boolean checkDeviceInfo(DeviceInfoDto deviceInfo,String clientId,String userName,String passwd){
		if(Objects.equals(userName, deviceInfo.getUserName()) && Objects.equals(passwd, deviceInfo.getPassword())){
			return true;
		}else {
			logger.warn("接入设备："+clientId+"；账号或密码错误，禁止该设备接入");
			return false;
		}
	}

	/**
	 * 再次读取Redis重新加载本地设备信息集合内容获取设备信息，过滤出指定clientId的设备信息
	 * @param clientId
	 * @return
	 */
	private DeviceInfoDto GetDeviceInfoByRedis(String clientId){
		this.InitDeviceInfoListByRedis();
		if(deviceInfoDtos!=null){
			Optional<DeviceInfoDto> optionalDeviceInfoDto = deviceInfoDtos.stream().distinct().filter(x -> x.getDeviceCode().equals(clientId)).findFirst();
			return optionalDeviceInfoDto.orElse(null);
		}else {
			return null;
		}
	}

}
