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

import com.dji.sample.component.mqtt.config.MqttPropertyConfiguration;
import com.dji.sample.component.mqtt.model.EventsReceiver;
import com.dji.sample.component.mqtt.model.MapKeyConst;
import com.dji.sample.component.redis.RedisConst;
import com.dji.sample.component.redis.RedisOpsUtils;
import com.dji.sample.component.websocket.service.IWebSocketMessageService;
import com.dji.sample.control.model.dto.JwtAclDTO;
import com.dji.sample.control.model.enums.DroneAuthorityEnum;
import com.dji.sample.control.model.enums.MqttAclAccessEnum;
import com.dji.sample.control.model.param.DrcConnectParam;
import com.dji.sample.control.model.param.DrcModeParam;
import com.dji.sample.control.service.IControlService;
import com.dji.sample.control.service.IManageDeviceOperatingRecordService;
import com.dji.sample.control.service.IDrcService;
import com.dji.sample.manage.model.dto.DeviceDTO;
import com.dji.sample.manage.service.IDeviceRedisService;
import com.dji.sample.manage.service.IDeviceService;
import com.dji.sample.wayline.model.enums.WaylineJobStatusEnum;
import com.dji.sample.wayline.model.enums.WaylineTaskStatusEnum;
import com.dji.sample.wayline.model.param.UpdateJobParam;
import com.dji.sample.wayline.service.IFlightTaskService;
import com.dji.sample.wayline.service.IWaylineJobService;
import com.dji.sample.wayline.service.IWaylineRedisService;
import com.dji.sdk.cloudapi.control.ControlErrorCodeEnum;
import com.dji.sdk.cloudapi.control.ControlMethodEnum;
import com.dji.sdk.cloudapi.control.DrcModeEnterRequest;
import com.dji.sdk.cloudapi.control.DrcModeMqttBroker;
import com.dji.sdk.cloudapi.control.api.AbstractControlService;
import com.dji.sdk.cloudapi.device.DeviceDomainEnum;
import com.dji.sdk.cloudapi.device.DockModeCodeEnum;
import com.dji.sdk.cloudapi.device.OsdDock;
import com.dji.sdk.cloudapi.device.OsdDockDrone;
import com.dji.sdk.cloudapi.device.OsdRcDrone;
import com.dji.sdk.cloudapi.wayline.FlighttaskProgress;
import com.dji.sdk.common.HttpResultResponse;
import com.dji.sdk.common.SDKManager;
import com.dji.sdk.mqtt.TopicConst;
import com.dji.sdk.mqtt.services.ServicesReplyData;
import com.dji.sdk.mqtt.services.TopicServicesResponse;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import static com.dji.sample.common.constant.DeviceConstant.CUSTOM_DOCK_LIST;
import static com.dji.sample.common.constant.DeviceConstant.CUSTOM_DOCK_START;

/**
 * @author sean
 * @version 1.3
 * @date 2023/1/11
 */
@Service
@Slf4j
public class DrcServiceImpl implements IDrcService {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private IWaylineJobService waylineJobService;

    @Autowired
    private IFlightTaskService flighttaskService;

    @Autowired
    private IDeviceService deviceService;
    
    @Autowired
    private ObjectMapper mapper;
    
    @Autowired
    private IWebSocketMessageService webSocketMessageService;

    @Autowired
    private IControlService controlService;

    @Autowired
    private IDeviceRedisService deviceRedisService;

    @Autowired
    private IWaylineRedisService waylineRedisService;

    @Autowired
    private AbstractControlService abstractControlService;

    @Resource
    private IManageDeviceOperatingRecordService manageDeviceOperatingRecordService;

    @Override
    public void setDrcModeInRedis(String dockSn, String clientId) {
        RedisOpsUtils.setWithExpire(RedisConst.DRC_PREFIX + dockSn, clientId, RedisConst.DRC_MODE_ALIVE_SECOND);
    }

    @Override
    public String getDrcModeInRedis(String dockSn) {
        return (String) RedisOpsUtils.get(RedisConst.DRC_PREFIX + dockSn);
    }

    @Override
    public Boolean delDrcModeInRedis(String dockSn) {
        return RedisOpsUtils.del(RedisConst.DRC_PREFIX + dockSn);
    }

    @Override
    public DrcModeMqttBroker userDrcAuth(String workspaceId, String userId, String username, DrcConnectParam param) {

        manageDeviceOperatingRecordService.saveOperatingRecord(null, "drc_connect", param.toString());

        // refresh token
        String clientId = param.getClientId();
        // first time
        if (!StringUtils.hasText(clientId) || !RedisOpsUtils.checkExist(RedisConst.MQTT_ACL_PREFIX + clientId)) {
            clientId = userId + "-" + System.currentTimeMillis();
            RedisOpsUtils.hashSet(RedisConst.MQTT_ACL_PREFIX + clientId, "", MqttAclAccessEnum.ALL.getValue());
        }

        String key = RedisConst.MQTT_ACL_PREFIX + clientId;

        try {
            RedisOpsUtils.expireKey(key, RedisConst.DRC_MODE_ALIVE_SECOND);

            return MqttPropertyConfiguration.getMqttBrokerWithDrc(
                    clientId, username, param.getExpireSec(), Collections.emptyMap());
        } catch (RuntimeException e) {
            RedisOpsUtils.del(key);
            throw e;
        }
    }

    private void checkDrcModeCondition(String workspaceId, String dockSn) {
        Optional<EventsReceiver<FlighttaskProgress>> runningOpt = waylineRedisService.getRunningWaylineJob(dockSn);
        if (runningOpt.isPresent() && WaylineJobStatusEnum.IN_PROGRESS == waylineJobService.getWaylineState(dockSn)) {
            flighttaskService.updateJobStatus(workspaceId, runningOpt.get().getBid(),
                    UpdateJobParam.builder().status(WaylineTaskStatusEnum.PAUSE).build());
        }

        Optional<DeviceDTO> dockOpt = deviceRedisService.getDeviceOnline(dockSn);
        if (dockOpt.isEmpty()) {
            throw new RuntimeException("The gateway is offline.");
        }
        DeviceDTO gateway = dockOpt.get();

        // Pilot RC：无 Dock mode / drone_in_dock；校验子机在线后申请云控授权并抢权
        if (DeviceDomainEnum.REMOTER_CONTROL == gateway.getDomain()) {
            String childSn = gateway.getChildDeviceSn();
            if (!StringUtils.hasText(childSn) || !deviceRedisService.checkDeviceOnline(childSn)) {
                throw new RuntimeException("Aircraft under RC is offline.");
            }
            boolean droneAirborne = deviceRedisService.getDeviceOsd(childSn, OsdRcDrone.class)
                    .map(osd -> osd.getElevation() != null && osd.getElevation() > 0)
                    .orElse(false);
            if (!droneAirborne) {
                // 地面也可进 DRC（负载控制），不强制在空；仅打日志
                log.info("Pilot RC DRC enter while aircraft may be on ground, sn={}", dockSn);
            }
            HttpResultResponse auth = controlService.requestCloudControlAuth(dockSn);
            if (HttpResultResponse.CODE_SUCCESS != auth.getCode()) {
                throw new IllegalArgumentException(auth.getMessage());
            }
            return;
        }

        DockModeCodeEnum dockMode = deviceService.getDockMode(dockSn);
        if (DockModeCodeEnum.IDLE == dockMode || DockModeCodeEnum.WORKING == dockMode) {
            Optional<OsdDockDrone> deviceOsd = deviceRedisService.getDeviceOsd(gateway.getChildDeviceSn(), OsdDockDrone.class);
            Optional<OsdDock> dockOsd = deviceRedisService.getDeviceOsd(dockSn, OsdDock.class);
            if (deviceOsd.isEmpty() || dockOsd.isEmpty() || dockOsd.get().getDroneInDock()) {
                throw new RuntimeException("The drone is not in the sky and cannot enter command flight mode.");
            }
        } else {
            throw new RuntimeException("The current state of the dock does not support entering command flight mode.");
        }

        HttpResultResponse result = controlService.seizeAuthority(dockSn, DroneAuthorityEnum.FLIGHT, null);
        if (HttpResultResponse.CODE_SUCCESS != result.getCode()) {
            throw new IllegalArgumentException(result.getMessage());
        }

    }

    @Override
    public JwtAclDTO deviceDrcEnter(String workspaceId, DrcModeParam param) {

        manageDeviceOperatingRecordService.saveOperatingRecord(param.getDockSn(), ControlMethodEnum.DRC_MODE_ENTER.getMethod(), param.toString());

        String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + param.getDockSn() + TopicConst.DRC;
        String pubTopic = topic + TopicConst.DOWN;
        String subTopic = topic + TopicConst.UP;

        // If the dock is in drc mode, refresh the permissions directly.
        if (deviceService.checkDockDrcMode(param.getDockSn())
                && param.getClientId().equals(this.getDrcModeInRedis(param.getDockSn()))) {
            refreshAcl(param.getDockSn(), param.getClientId(), topic, subTopic);
            return JwtAclDTO.builder().sub(List.of(subTopic)).pub(List.of(pubTopic)).build();
        }

        checkDrcModeCondition(workspaceId, param.getDockSn());

        TopicServicesResponse<ServicesReplyData> reply = abstractControlService.drcModeEnter(
                SDKManager.getDeviceSDK(param.getDockSn()),
                new DrcModeEnterRequest()
                        // 设备侧必须用官方 host:port 格式，不能用 Web 的 wss://.../mqtt
                        .setMqttBroker(MqttPropertyConfiguration.getMqttBrokerWithDrcForDevice(
                                param.getDockSn() + "-" + System.currentTimeMillis(), param.getDockSn(),
                                RedisConst.DRC_MODE_ALIVE_SECOND.longValue(),
                                Map.of(MapKeyConst.ACL, objectMapper.convertValue(JwtAclDTO.builder()
                                        .pub(List.of(subTopic))
                                        .sub(List.of(pubTopic))
                                        .build(), new TypeReference<Map<String, ?>>() {}))))
                        .setHsiFrequency(1).setOsdFrequency(10));

        if (!reply.getData().getResult().isSuccess()) {
            Integer code = reply.getData().getResult().getCode();
            String hint = "SN: " + param.getDockSn() + "; Error:" + reply.getData().getResult()
                    + "; Failed to enter command flight control mode, please try again later!";
            if (code != null && code == ControlErrorCodeEnum.DRC_LINK_REFUSED.getCode()) {
                hint += " (514304: 设备连不上 DRC Broker。"
                        + "请在 mqtt.DRC 配置 device-host/device-port 为公网可达的 MQTTS，例如 host:8883、device-enable-tls=true；"
                        + "官方地址格式为 host:port，不要带 wss://。)";
            }
            throw new RuntimeException(hint);
        }

        refreshAcl(param.getDockSn(), param.getClientId(), pubTopic, subTopic);
        return JwtAclDTO.builder().sub(List.of(subTopic)).pub(List.of(pubTopic)).build();
    }

    private void refreshAcl(String dockSn, String clientId, String pubTopic, String subTopic) {
        this.setDrcModeInRedis(dockSn, clientId);

        // assign acl，Match by clientId. https://www.emqx.io/docs/zh/v4.4/advanced/acl-redis.html
        // scheme: HSET mqtt_acl:[clientid] [topic] [access]
        String key = RedisConst.MQTT_ACL_PREFIX + clientId;
        RedisOpsUtils.hashSet(key, pubTopic, MqttAclAccessEnum.PUB.getValue());
        RedisOpsUtils.hashSet(key, subTopic, MqttAclAccessEnum.SUB.getValue());
        RedisOpsUtils.expireKey(key, RedisConst.DRC_MODE_ALIVE_SECOND);
    }

    @Override
    public void deviceDrcExit(String workspaceId, DrcModeParam param) {

        manageDeviceOperatingRecordService.saveOperatingRecord(param.getDockSn(), ControlMethodEnum.DRC_MODE_EXIT.getMethod(), param.toString());

        if (!deviceService.checkDockDrcMode(param.getDockSn())) {
            throw new RuntimeException("The dock is not in flight control mode.");
        }
        TopicServicesResponse<ServicesReplyData> reply =
                abstractControlService.drcModeExit(SDKManager.getDeviceSDK(param.getDockSn()));
        if (!reply.getData().getResult().isSuccess()) {
            Integer code = reply.getData().getResult().getCode();
            // 官方错误码 514300–514304：网关/网络断开或连接失败，设备侧已无有效 DRC
            // 见 docs/dji-cloud-api/ERROR-CODE-NOTES.md 与 cn|en/error-code.md
            if (isDrcLinkAlreadyGone(code)) {
                log.warn("drc_mode_exit: DRC/gateway link already gone ({}), treat as exited. sn={}",
                        code, param.getDockSn());
            } else {
                throw new RuntimeException("SN: " + param.getDockSn() + "; Error:" +
                        reply.getData().getResult() + "; Failed to exit command flight control mode, please try again later!");
            }
        }

        String jobId = waylineRedisService.getPausedWaylineJobId(param.getDockSn());
        if (StringUtils.hasText(jobId)) {
            if (CUSTOM_DOCK_LIST.contains(param.getDockSn()) || param.getDockSn().contains("12345")) {

            } else if (StringUtils.hasText(param.getDockSn()) && param.getDockSn().startsWith(CUSTOM_DOCK_START)) {

            } else {
                // 非自定义机场 恢复航线
                flighttaskService.updateJobStatus(workspaceId, jobId, UpdateJobParam.builder().status(WaylineTaskStatusEnum.RESUME).build());
            }
        }

        this.delDrcModeInRedis(param.getDockSn());
        RedisOpsUtils.del(RedisConst.MQTT_ACL_PREFIX + param.getClientId());

        // Pilot：退出 DRC 后释放云端控制授权
        Optional<DeviceDTO> gw = deviceRedisService.getDeviceOnline(param.getDockSn());
        if (gw.isPresent() && DeviceDomainEnum.REMOTER_CONTROL == gw.get().getDomain()) {
            try {
                controlService.releaseCloudControlAuth(param.getDockSn());
            } catch (Exception e) {
                log.warn("release cloud control auth failed for {}: {}", param.getDockSn(), e.getMessage());
            }
        }
    }

    /**
     * 官方 Error Code 514300–514304：网关异常 / 超时断开 / 证书失败 / 网络断开 / 连接被拒。
     * 退出 DRC 时表示链路已不可用，按已退出处理。
     */
    private static boolean isDrcLinkAlreadyGone(Integer code) {
        if (code == null) {
            return false;
        }
        return code == ControlErrorCodeEnum.DRC_ABNORMAL.getCode()
                || code == ControlErrorCodeEnum.DRC_HEARTBEAT_TIMED_OUT.getCode()
                || code == ControlErrorCodeEnum.DRC_CERTIFICATE_ABNORMAL.getCode()
                || code == ControlErrorCodeEnum.DRC_LINK_LOST.getCode()
                || code == ControlErrorCodeEnum.DRC_LINK_REFUSED.getCode();
    }

}
