package com.dji.sample.manage.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.dji.sample.component.websocket.model.BizCodeEnum;
import com.dji.sample.component.websocket.service.IWebSocketMessageService;
import com.dji.sample.control.model.enums.DroneAuthorityEnum;
import com.dji.sample.manage.dao.IDevicePayloadMapper;
import com.dji.sample.manage.model.dto.*;
import com.dji.sample.manage.model.entity.DevicePayloadEntity;
import com.dji.sample.manage.model.enums.UserTypeEnum;
import com.dji.sample.manage.service.ICapacityCameraService;
import com.dji.sample.manage.service.IDeviceDictionaryService;
import com.dji.sample.manage.service.IDevicePayloadService;
import com.dji.sample.manage.service.IDeviceRedisService;
import com.dji.sdk.cloudapi.device.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author sean.zhou
 * @version 0.1
 * @date 2021/11/19
 */
@Slf4j
@Service
@Transactional
public class DevicePayloadServiceImpl implements IDevicePayloadService {

    @Autowired
    private IDevicePayloadMapper mapper;

    @Autowired
    private IDeviceDictionaryService dictionaryService;

    @Autowired
    private ICapacityCameraService capacityCameraService;

    @Autowired
    private IWebSocketMessageService sendMessageService;

    @Autowired
    private IDeviceRedisService deviceRedisService;

    @Override
    public Integer checkPayloadExist(String payloadSn) {
        DevicePayloadEntity devicePayload = mapper.selectOne(
                new LambdaQueryWrapper<DevicePayloadEntity>()
                        .eq(DevicePayloadEntity::getPayloadSn, payloadSn));
        return devicePayload != null ? devicePayload.getId() : -1;
    }

    private Integer saveOnePayloadEntity(DevicePayloadEntity entity) {
        int id = this.checkPayloadExist(entity.getPayloadSn());
        // If it already exists, update the data directly.
        if (id > 0) {
            entity.setId(id);
            // For the payload of the drone itself, there is no firmware version.
            entity.setFirmwareVersion(null);
            return mapper.updateById(entity) > 0 ? entity.getId() : 0;
        }
        return mapper.insert(entity) > 0 ? entity.getId() : 0;
    }

    @Override
    public Boolean savePayloadDTOs(DeviceDTO device, List<DevicePayloadReceiver> payloadReceiverList) {
        Map<String, ControlSourceEnum> controlMap = CollectionUtils.isEmpty(device.getPayloadsList()) ?
                Collections.emptyMap() : device.getPayloadsList().stream()
                    .collect(Collectors.toMap(DevicePayloadDTO::getPayloadSn, DevicePayloadDTO::getControlSource));

        for (DevicePayloadReceiver payloadReceiver : payloadReceiverList) {
            payloadReceiver.setDeviceSn(device.getDeviceSn());
            int payloadId = this.saveOnePayloadDTO(payloadReceiver);
            if (payloadId <= 0) {
                log.error("Payload data saving failed.");
                return false;
            }
            if (controlMap.get(payloadReceiver.getSn()) != payloadReceiver.getControlSource()) {
                sendMessageService.sendBatch(device.getWorkspaceId(), UserTypeEnum.WEB.getVal(),
                                    BizCodeEnum.CONTROL_SOURCE_CHANGE.getCode(),
                                    DeviceAuthorityDTO.builder()
                                            .controlSource(payloadReceiver.getControlSource())
                                            .sn(payloadReceiver.getSn())
                                            .type(DroneAuthorityEnum.PAYLOAD)
                                            .build());
            }
        }

        List<DevicePayloadDTO> payloads = this.getDevicePayloadEntitiesByDeviceSn(device.getDeviceSn());
        device.setPayloadsList(payloads);
        deviceRedisService.setDeviceOnline(device);
        return true;
    }

    @Override
    public Integer saveOnePayloadDTO(DevicePayloadReceiver payloadReceiver) {
        return this.saveOnePayloadEntity(receiverConvertToEntity(payloadReceiver));
    }

    @Override
    public List<DevicePayloadDTO> getDevicePayloadEntitiesByDeviceSn(String deviceSn) {
        return mapper.selectList(
                new LambdaQueryWrapper<DevicePayloadEntity>()
                        .eq(DevicePayloadEntity::getDeviceSn, deviceSn))
                .stream()
                .map(this::payloadEntityConvertToDTO)
                .collect(Collectors.toList());
    }

    @Override
    public void deletePayloadsByDeviceSn(List<String> deviceSns) {
        deviceSns.forEach(deviceSn -> {
            mapper.delete(
                    new LambdaQueryWrapper<DevicePayloadEntity>()
                            .eq(DevicePayloadEntity::getDeviceSn, deviceSn));
            capacityCameraService.deleteCapacityCameraByDeviceSn(deviceSn);
        });
    }

    @Override
    public Boolean updateFirmwareVersion(String droneSn, PayloadFirmwareVersion receiver) {
        return mapper.update(DevicePayloadEntity.builder()
                        .firmwareVersion(receiver.getFirmwareVersion()).build(),
                new LambdaUpdateWrapper<DevicePayloadEntity>()
                        .eq(DevicePayloadEntity::getDeviceSn, droneSn)
                        .eq(DevicePayloadEntity::getPayloadSn, droneSn + "-" + receiver.getPosition().getPosition())
        ) > 0;
    }

    /**
     * Handle payload data for devices.
     * @param drone
     * @param payloads
     */
    public void updatePayloadControl(DeviceDTO drone, List<DevicePayloadReceiver> payloads) {
        // 检查设备是否在线，不在线则不更新payload，避免异常离线导致数据丢失
        if (!deviceRedisService.checkDeviceOnline(drone.getDeviceSn())) {
            log.warn("Device {} is offline, skipping payload update.", drone.getDeviceSn());
            return;
        }

        boolean match = payloads.stream().peek(p -> p.setSn(Objects.requireNonNullElse(p.getSn(),
                p.getDeviceSn() + "-" + p.getPayloadIndex().getPosition().getPosition())))
                .anyMatch(p -> ControlSourceEnum.UNKNOWN == p.getControlSource());
        if (match) {
            return;
        }

        if (payloads.isEmpty()) {
            drone.setPayloadsList(null);
            // this.deletePayloadsByDeviceSn(List.of(drone.getDeviceSn()));
            deviceRedisService.setDeviceOnline(drone);
            return;
        }

        // Filter unsaved payload information.
        Set<String> payloadSns = this.getDevicePayloadEntitiesByDeviceSn(drone.getDeviceSn())
                .stream().map(DevicePayloadDTO::getPayloadSn).collect(Collectors.toSet());

        Set<String> newPayloadSns = payloads.stream().map(DevicePayloadReceiver::getSn).collect(Collectors.toSet());
        payloadSns.removeAll(newPayloadSns);
        this.deletePayloadsByPayloadsSn(payloadSns);

        // Save the new payload information.
        boolean isSave = this.savePayloadDTOs(drone, payloads);
        log.debug("The result of saving the payloads is {}.", isSave);
    }

    @Override
    public void deletePayloadsByPayloadsSn(Collection<String> payloadSns) {
        if (CollectionUtils.isEmpty(payloadSns)) {
            return;
        }
        mapper.delete(new LambdaUpdateWrapper<DevicePayloadEntity>()
                .or(wrapper -> payloadSns.forEach(sn -> wrapper.eq(DevicePayloadEntity::getPayloadSn, sn))));
    }

    @Override
    public Boolean checkAuthorityPayload(String deviceSn, String payloadIndex) {
        return deviceRedisService.getDeviceOnline(deviceSn).flatMap(device ->
                Optional.of(DeviceDomainEnum.DRONE == device.getDomain()
                        && !CollectionUtils.isEmpty(device.getPayloadsList())
                        && ControlSourceEnum.A ==
                        device.getPayloadsList().stream()
                                .filter(payload -> payloadIndex.equals(payload.getPayloadIndex().toString()))
                                .map(DevicePayloadDTO::getControlSource).findAny()
                                .orElse(ControlSourceEnum.B)))
                .orElse(true);
    }

    /**
     * Convert database entity objects into payload data transfer object.
     * @param entity
     * @return
     */
    private DevicePayloadDTO payloadEntityConvertToDTO(DevicePayloadEntity entity) {
        DevicePayloadDTO.DevicePayloadDTOBuilder builder = DevicePayloadDTO.builder();
        if (entity != null) {
            builder.payloadSn(entity.getPayloadSn())
                    .payloadName(entity.getPayloadName())
                    .payloadDesc(entity.getPayloadDesc())
                    .index(entity.getPayloadIndex())
                    .payloadIndex(new PayloadIndex()
                            .setType(DeviceTypeEnum.find(entity.getPayloadType()))
                            .setSubType(DeviceSubTypeEnum.find(entity.getSubType()))
                            .setPosition(PayloadPositionEnum.find(entity.getPayloadIndex())))
                    .controlSource(ControlSourceEnum.find(entity.getControlSource()));
        }
        return builder.build();
    }

    /**
     * Convert the received payload object into a database entity object.
     * @param dto   payload
     * @return
     */
    private DevicePayloadEntity receiverConvertToEntity(DevicePayloadReceiver dto) {
        if (dto == null) {
            return new DevicePayloadEntity();
        }
        DevicePayloadEntity.DevicePayloadEntityBuilder builder = DevicePayloadEntity.builder();

        // The cameraIndex consists of type and subType and the index of the payload hanging on the drone.
        // type-subType-index
        Optional<DeviceDictionaryDTO> dictionaryOpt = dictionaryService.getOneDictionaryInfoByTypeSubType(
                DeviceDomainEnum.PAYLOAD.getDomain(), dto.getPayloadIndex().getType().getType(),
                dto.getPayloadIndex().getSubType().getSubType());
        dictionaryOpt.ifPresent(dictionary ->
                builder.payloadName(dictionary.getDeviceName())
                        .payloadDesc(dictionary.getDeviceDesc()));

        builder.payloadType(dto.getPayloadIndex().getType().getType())
                .subType(dto.getPayloadIndex().getSubType().getSubType())
                .payloadIndex(dto.getPayloadIndex().getPosition().getPosition())
                .controlSource(dto.getControlSource().getControlSource());

        return builder
                .payloadSn(dto.getSn())
                .deviceSn(dto.getDeviceSn())
                .build();
    }

    private DevicePayloadDTO receiver2Dto(DevicePayloadReceiver receiver) {
        DevicePayloadDTO.DevicePayloadDTOBuilder builder = DevicePayloadDTO.builder();
        if (receiver == null) {
            return builder.build();
        }
        return builder.payloadSn(receiver.getSn())
                .payloadIndex(receiver.getPayloadIndex())
                .controlSource(receiver.getControlSource())
                .build();
    }

}