package com.lingniu.ingest.gateway;

import com.lingniu.ingest.protocol.jt808.downlink.Jt808Commands;
import com.lingniu.ingest.protocol.jt808.model.Jt808Message;
import com.lingniu.ingest.protocol.jt1078.downlink.Jt1078Commands;
import com.lingniu.ingest.session.CommandDispatcher;
import com.lingniu.ingest.session.DeviceSession;
import com.lingniu.ingest.session.SessionStore;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;

/**
 * 下行命令 REST 入口。命令通过 {@link CommandDispatcher} 发往协议模块，
 * 由 protocol-jt808 的 {@code Jt808CommandDispatcher} 实际发送到终端。
 *
 * <p>路径参数 {@code clientId} 可以是 sessionId / phone / vin，
 * 由 {@link SessionStore} 三索引解析。
 */
@RestController
@RequestMapping("/terminal")
public class TerminalCommandController {

    private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(20);

    private final SessionStore sessions;
    private final CommandDispatcher dispatcher;

    public TerminalCommandController(SessionStore sessions, CommandDispatcher dispatcher) {
        this.sessions = sessions;
        this.dispatcher = dispatcher;
    }

    @GetMapping("/session")
    public CommandResult session(@RequestParam String clientId) {
        Optional<DeviceSession> s = resolveSession(clientId);
        return s.<CommandResult>map(CommandResult::ok)
                .orElseGet(() -> CommandResult.failure(404, "session not found: " + clientId));
    }

    @GetMapping("/location")
    public CommandResult queryLocation(@RequestParam String clientId) {
        return awaitSync(clientId, Jt808Commands.queryLocation(), "query-location");
    }

    @GetMapping("/parameters")
    public CommandResult queryParameters(@RequestParam String clientId) {
        return awaitSync(clientId, Jt808Commands.queryParameters(), "query-parameters");
    }

    @PutMapping("/parameters")
    public CommandResult setParameters(@RequestParam String clientId,
                                       @RequestBody Map<String, String> body) {
        List<Jt808Commands.ParamItem> items = body.entrySet().stream()
                .map(e -> new Jt808Commands.ParamItem(
                        parseId(e.getKey()),
                        e.getValue() == null ? new byte[0] : e.getValue().getBytes(StandardCharsets.UTF_8)))
                .collect(Collectors.toList());
        return awaitSync(clientId, Jt808Commands.setParameters(items), "set-parameters");
    }

    @PostMapping("/control")
    public CommandResult terminalControl(@RequestParam String clientId,
                                         @RequestBody Map<String, Object> body) {
        int word = Integer.parseInt(body.getOrDefault("command", "0").toString());
        String params = body.getOrDefault("params", "").toString();
        return awaitSync(clientId, Jt808Commands.terminalControl(word, params), "terminal-control");
    }

    @PostMapping("/ack")
    public CommandResult ack(@RequestParam String clientId, @RequestBody Map<String, Integer> body) {
        int ackSerial = body.getOrDefault("ackSerial", 0);
        int ackMsgId = body.getOrDefault("ackMsgId", 0);
        int result = body.getOrDefault("result", 0);
        return fireAndForget(clientId, Jt808Commands.platformAck(ackSerial, ackMsgId, result), "platform-ack");
    }

    @GetMapping("/attributes")
    public CommandResult attributes(@RequestParam String clientId) {
        return awaitSync(clientId, Jt808Commands.queryAttributes(), "query-attributes");
    }

    @DeleteMapping("/area")
    public CommandResult deleteArea(@RequestParam String clientId, @RequestParam String ids) {
        return awaitSync(clientId, Jt808Commands.deleteArea(parseIds(ids)), "delete-area");
    }

    @PostMapping("/alarm_ack")
    public CommandResult alarmAck(@RequestParam String clientId, @RequestBody Map<String, Object> body) {
        int responseSerialNo = Integer.parseInt(body.getOrDefault("responseSerialNo", "0").toString());
        long type = parseLong(body.getOrDefault("type", "0").toString());
        return fireAndForget(clientId, Jt808Commands.alarmAck(responseSerialNo, type), "alarm-ack");
    }

    @GetMapping("/jt1078/attributes")
    public CommandResult jt1078Attributes(@RequestParam String clientId) {
        return awaitSync(clientId, Jt1078Commands.queryMediaAttributes(), "jt1078-query-media-attributes");
    }

    @PostMapping("/jt1078/realtime")
    public CommandResult jt1078RealtimePlay(@RequestParam String clientId,
                                            @RequestBody Map<String, Object> body) {
        return awaitSync(clientId, Jt1078Commands.realtimePlay(
                stringValue(body, "ip", ""),
                intValue(body, "tcpPort", 0),
                intValue(body, "udpPort", 0),
                intValue(body, "channelNo", 0),
                intValue(body, "mediaType", 0),
                intValue(body, "streamType", 0)), "jt1078-realtime-play");
    }

    @PostMapping("/jt1078/realtime_control")
    public CommandResult jt1078RealtimeControl(@RequestParam String clientId,
                                               @RequestBody Map<String, Object> body) {
        return awaitSync(clientId, Jt1078Commands.realtimeControl(
                intValue(body, "channelNo", 0),
                intValue(body, "command", 0),
                intValue(body, "closeType", 0),
                intValue(body, "streamType", 0)), "jt1078-realtime-control");
    }

    @PostMapping("/jt1078/history")
    public CommandResult jt1078HistoryPlay(@RequestParam String clientId,
                                           @RequestBody Map<String, Object> body) {
        return awaitSync(clientId, Jt1078Commands.historyPlay(
                stringValue(body, "ip", ""),
                intValue(body, "tcpPort", 0),
                intValue(body, "udpPort", 0),
                intValue(body, "channelNo", 0),
                intValue(body, "mediaType", 0),
                intValue(body, "streamType", 0),
                intValue(body, "storageType", 0),
                intValue(body, "playbackMode", 0),
                intValue(body, "playbackSpeed", 0),
                stringValue(body, "startTime", ""),
                stringValue(body, "endTime", "")), "jt1078-history-play");
    }

    @PostMapping("/jt1078/history_control")
    public CommandResult jt1078HistoryControl(@RequestParam String clientId,
                                              @RequestBody Map<String, Object> body) {
        return awaitSync(clientId, Jt1078Commands.historyControl(
                intValue(body, "channelNo", 0),
                intValue(body, "playbackMode", 0),
                intValue(body, "playbackSpeed", 0),
                stringValue(body, "playbackTime", "")), "jt1078-history-control");
    }

    @PostMapping("/jt1078/resource_search")
    public CommandResult jt1078ResourceSearch(@RequestParam String clientId,
                                              @RequestBody Map<String, Object> body) {
        return awaitSync(clientId, Jt1078Commands.resourceSearch(
                intValue(body, "channelNo", 0),
                stringValue(body, "startTime", ""),
                stringValue(body, "endTime", ""),
                longValue(body, "warnBit1", 0),
                longValue(body, "warnBit2", 0),
                intValue(body, "mediaType", 0),
                intValue(body, "streamType", 0),
                intValue(body, "storageType", 0)), "jt1078-resource-search");
    }

    @PostMapping("/jt1078/file_upload")
    public CommandResult jt1078FileUpload(@RequestParam String clientId,
                                          @RequestBody Map<String, Object> body) {
        return awaitSync(clientId, Jt1078Commands.fileUpload(
                stringValue(body, "ip", ""),
                intValue(body, "port", 0),
                stringValue(body, "username", ""),
                stringValue(body, "password", ""),
                stringValue(body, "path", ""),
                intValue(body, "channelNo", 0),
                stringValue(body, "startTime", ""),
                stringValue(body, "endTime", ""),
                longValue(body, "warnBit1", 0),
                longValue(body, "warnBit2", 0),
                intValue(body, "mediaType", 0),
                intValue(body, "streamType", 0),
                intValue(body, "storageType", 0),
                intValue(body, "condition", 0)), "jt1078-file-upload");
    }

    @PostMapping("/jt1078/file_upload_control")
    public CommandResult jt1078FileUploadControl(@RequestParam String clientId,
                                                 @RequestBody Map<String, Object> body) {
        return awaitSync(clientId, Jt1078Commands.fileUploadControl(
                intValue(body, "responseSerialNo", 0),
                intValue(body, "command", 0)), "jt1078-file-upload-control");
    }

    // ===== helpers =====

    private Optional<DeviceSession> resolveSession(String clientId) {
        // 运维侧常拿到 VIN、手机号或 sessionId 中任意一个；这里按三索引兜底解析到在线连接。
        return sessions.findBySessionId(clientId)
                .or(() -> sessions.findByPhone(clientId))
                .or(() -> sessions.findByVin(clientId));
    }

    private CommandResult awaitSync(String clientId, Jt808Commands.DownlinkCommand cmd, String op) {
        // 需要终端应答的命令走 request-response，超时时间由网关统一兜底，避免 HTTP 线程无限等待。
        CompletableFuture<Jt808Message> future =
                dispatcher.request(clientId, cmd, Jt808Message.class, DEFAULT_TIMEOUT);
        try {
            Jt808Message resp = future.join();
            return CommandResult.ok(Map.of(
                    "messageId", "0x" + Integer.toHexString(resp.header().messageId()),
                    "serial", resp.header().serialNo(),
                    "phone", resp.header().phone()));
        } catch (CompletionException e) {
            Throwable cause = e.getCause() != null ? e.getCause() : e;
            return CommandResult.failure(500, op + " failed: " + cause.getMessage());
        } catch (Exception e) {
            return CommandResult.failure(500, op + " failed: " + e.getMessage());
        }
    }

    private CommandResult fireAndForget(String clientId, Jt808Commands.DownlinkCommand cmd, String op) {
        try {
            // ACK 类命令不等待终端回包；只确认写入在线 session 对应的 channel。
            dispatcher.notify(clientId, cmd).join();
            return CommandResult.ok(Map.of("op", op));
        } catch (Exception e) {
            return CommandResult.failure(500, op + " failed: " + e.getMessage());
        }
    }

    private static long parseId(String key) {
        if (key == null || key.isBlank()) return 0;
        String k = key.startsWith("0x") ? key.substring(2) : key;
        return Long.parseLong(k, 16);
    }

    private static List<Long> parseIds(String ids) {
        if (ids == null || ids.isBlank()) {
            return List.of();
        }
        return java.util.Arrays.stream(ids.split(","))
                .map(String::trim)
                .filter(s -> !s.isBlank())
                .map(TerminalCommandController::parseLong)
                .toList();
    }

    private static long parseLong(String value) {
        if (value == null || value.isBlank()) return 0;
        String v = value.startsWith("0x") || value.startsWith("0X") ? value.substring(2) : value;
        return value.startsWith("0x") || value.startsWith("0X")
                ? Long.parseLong(v, 16)
                : Long.parseLong(v);
    }

    private static String stringValue(Map<String, Object> body, String key, String defaultValue) {
        Object value = body.get(key);
        return value == null ? defaultValue : value.toString();
    }

    private static int intValue(Map<String, Object> body, String key, int defaultValue) {
        Object value = body.get(key);
        return value == null ? defaultValue : (int) parseLong(value.toString());
    }

    private static long longValue(Map<String, Object> body, String key, long defaultValue) {
        Object value = body.get(key);
        return value == null ? defaultValue : parseLong(value.toString());
    }
}
