package com.market.websocket;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.market.enums.WsTopicType;
import com.market.model.ws.WsRequest;
import com.market.model.ws.WsResponse;
import com.market.service.KlineService;
import com.market.utils.SpotConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * 前端 WebSocket 连接处理器
 *
 * <p>支持命令：
 * <ul>
 *   <li>{@code sub}   — 订阅，支持单个或多个主题</li>
 *   <li>{@code unsub} — 取消订阅，支持单个或多个主题</li>
 *   <li>{@code req}   — kline 历史查询（需 from/to）；ticket 返回最新缓存数据</li>
 * </ul>
 *
 * <p>支持主题格式：
 * <pre>
 *   market.{symbol}.kline.{interval}   — K 线
 *   market.{symbol}.trade              — 实时成交
 *   market.{symbol}.ticket             — 24h Mini Ticker
 *   market.{symbol}.depth_mark         — 标记价格/资金费率
 * </pre>
 */
@Slf4j
@Component
public class FrontendWebSocketHandler extends TextWebSocketHandler {

    private final SubscriptionManager subscriptionManager;
    private final ObjectMapper objectMapper;
    private final KlineService klineService;

    public FrontendWebSocketHandler(SubscriptionManager subscriptionManager,
                                    ObjectMapper objectMapper,
                                    KlineService klineService) {
        this.subscriptionManager = subscriptionManager;
        this.objectMapper = objectMapper;
        this.klineService = klineService;
    }

    // ------------------------------------------------------------------ lifecycle

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        log.info("前端连接建立 session={} remote={}", session.getId(), session.getRemoteAddress());
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        subscriptionManager.removeSession(session);
        log.info("前端连接断开 session={} status={}", session.getId(), status);
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) {
        subscriptionManager.removeSession(session);
    }

    // ------------------------------------------------------------------ message

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        try {
            WsRequest req = objectMapper.readValue(message.getPayload(), WsRequest.class);
            if (req.getSub() != null && !req.getSub().isEmpty()) {
                handleSub(session, req);
            } else if (req.getUnsub() != null && !req.getUnsub().isEmpty()) {
                handleUnsub(session, req);
            } else if (req.getReq() != null && !req.getReq().isEmpty()) {
                handleReq(session, req);
            } else {
                sendError(session, req.getId(), SpotConstant.WS_ERROR_BAD_REQUEST, SpotConstant.MSG_UNKNOWN_ACTION);
            }
        } catch (Exception e) {
            log.warn("解析前端消息失败: {}", message.getPayload(), e);
        }
    }

    /**
     * 批量订阅：校验所有主题，有效的全部订阅，一次 ACK 返回已订阅列表；
     * 无效主题单独发送错误响应。
     */
    private void handleSub(WebSocketSession session, WsRequest req) throws IOException {
        List<String> ok = new ArrayList<>();
        for (String topic : req.getSub()) {
            if (!isValidTopic(topic)) {
                sendError(session, req.getId(), SpotConstant.WS_ERROR_BAD_REQUEST, "Invalid topic: " + topic);
                continue;
            }
            subscriptionManager.subscribe(session, topic);
            ok.add(topic);
        }
        if (!ok.isEmpty()) {
            send(session, WsResponse.builder()
                    .id(req.getId())
                    .status(SpotConstant.STATUS_OK)
                    .subbed(ok)
                    .ts(System.currentTimeMillis())
                    .build());
        }
    }

    /**
     * 批量取消：一次 ACK 返回已取消列表。
     */
    private void handleUnsub(WebSocketSession session, WsRequest req) throws IOException {
        List<String> removed = new ArrayList<>();
        for (String topic : req.getUnsub()) {
            subscriptionManager.unsubscribe(session, topic);
            removed.add(topic);
        }
        send(session, WsResponse.builder()
                .id(req.getId())
                .status(SpotConstant.STATUS_OK)
                .unsubbed(removed)
                .ts(System.currentTimeMillis())
                .build());
    }

    /**
     * req：支持批量，每个主题独立响应。
     * <ul>
     *   <li>kline  — 历史查询，from/to 必填，所有 kline 主题共用同一 from/to</li>
     *   <li>ticket — 返回最新缓存数据，无需 from/to</li>
     * </ul>
     */
    private void handleReq(WebSocketSession session, WsRequest req) throws IOException {
        for (String topic : req.getReq()) {
            if (isKlineTopic(topic)) {
                if (req.getFrom() == null || req.getTo() == null) {
                    sendError(session, req.getId(), SpotConstant.WS_ERROR_BAD_REQUEST, SpotConstant.MSG_FROM_TO_REQUIRED_PREFIX + topic);
                    continue;
                }
                handleKlineHistory(session, req, topic);
            } else if (isTicketTopic(topic)) {
                handleTicketReq(session, req, topic);
            } else {
                sendError(session, req.getId(), SpotConstant.WS_ERROR_BAD_REQUEST, SpotConstant.MSG_UNSUPPORTED_REQ_PREFIX + topic);
            }
        }
    }

    /**
     * ticket req：返回该 symbol 的最新缓存 ticker 数据。
     * topic 格式：market.{symbol}.ticket
     */
    private void handleTicketReq(WebSocketSession session, WsRequest req, String topic) throws IOException {
        Object latest = subscriptionManager.getLatest(topic);
        if (latest == null) {
            sendError(session, req.getId(), SpotConstant.WS_ERROR_NOT_FOUND, SpotConstant.MSG_NO_TICKER_PREFIX + topic);
            return;
        }
        send(session, WsResponse.builder()
                .id(req.getId())
                .status(SpotConstant.STATUS_OK)
                .rep(topic)
                .ts(System.currentTimeMillis())
                .data(latest)
                .build());
    }

    // ------------------------------------------------------------------ kline 历史查询

    /**
     * K 线历史查询：从 DB 查询 [from, to] 区间数据，一次性返回。
     * topic 格式：market.{symbol}.kline.{interval}
     */
    private void handleKlineHistory(WebSocketSession session, WsRequest req, String topic) throws IOException {
        String[] parts = topic.split(SpotConstant.TOPIC_SEPARATOR_REGEX);
        String symbol   = parts[SpotConstant.TOPIC_PART_INDEX_SYMBOL];   // btcusdt
        String interval = parts[SpotConstant.TOPIC_PART_INDEX_INTERVAL]; // 1m

        if (req.getFrom() > req.getTo()) {
            sendError(session, req.getId(), SpotConstant.WS_ERROR_BAD_REQUEST, SpotConstant.MSG_FROM_GT_TO);
            return;
        }

        List<?> history = klineService.queryHistory(symbol, interval, req.getFrom(), req.getTo());
        send(session, WsResponse.builder()
                .id(req.getId())
                .status(SpotConstant.STATUS_OK)
                .rep(topic)
                .ts(System.currentTimeMillis())
                .data(history)
                .build());
    }

    // ------------------------------------------------------------------ push（由 DataListener 调用）

    /**
     * 向订阅了 {@code topic} 的所有前端 session 推送数据，并缓存最新值（供 req 使用）。
     */
    public void push(String topic, Object tick) {
        subscriptionManager.cacheLatest(topic, tick);

        Set<WebSocketSession> sessions = subscriptionManager.getSessions(topic);
        if (sessions.isEmpty()) return;

        try {
            String json = objectMapper.writeValueAsString(
                    WsResponse.builder()
                            .ch(topic)
                            .ts(System.currentTimeMillis())
                            .tick(tick)
                            .build());
            TextMessage msg = new TextMessage(json);
            for (WebSocketSession session : sessions) {
                if (session.isOpen()) {
                    try {
                        sendSafe(session, msg);
                    } catch (IOException e) {
                        log.warn("推送失败 session={} topic={}", session.getId(), topic, e);
                    }
                }
            }
        } catch (Exception e) {
            log.warn("序列化失败 topic={}", topic, e);
        }
    }

    // ------------------------------------------------------------------ helpers

    private boolean isKlineTopic(String topic) {
        String[] parts = topic.split(SpotConstant.TOPIC_SEPARATOR_REGEX);
        return parts.length == SpotConstant.TOPIC_PARTS_KLINE
                && WsTopicType.KLINE.getCode().equals(parts[SpotConstant.TOPIC_PART_INDEX_TYPE]);
    }

    private boolean isTicketTopic(String topic) {
        String[] parts = topic.split(SpotConstant.TOPIC_SEPARATOR_REGEX);
        return parts.length == SpotConstant.TOPIC_PARTS_COMMON
                && SpotConstant.TOPIC_PREFIX_MARKET_DOT.equals(parts[SpotConstant.TOPIC_PART_INDEX_PREFIX])
                && WsTopicType.TICKET.getCode().equals(parts[SpotConstant.TOPIC_PART_INDEX_TYPE]);
    }

    private boolean isValidTopic(String topic) {
        if (topic == null || topic.isBlank()) return false;
        String[] parts = topic.split(SpotConstant.TOPIC_SEPARATOR_REGEX);
        if (parts.length < SpotConstant.TOPIC_PARTS_COMMON
                || !SpotConstant.TOPIC_PREFIX_MARKET_DOT.equals(parts[SpotConstant.TOPIC_PART_INDEX_PREFIX])) {
            return false;
        }
        WsTopicType type = WsTopicType.fromCode(parts[SpotConstant.TOPIC_PART_INDEX_TYPE]);
        if (type == null) {
            return false;
        }
        return switch (type) {
            case TRADE, TICKET, MARK, DEPTH -> parts.length == SpotConstant.TOPIC_PARTS_COMMON;
            case KLINE -> parts.length == SpotConstant.TOPIC_PARTS_KLINE;
        };
    }

    private void sendError(WebSocketSession session, String id, String errCode, String errMsg) {
        try {
            send(session, WsResponse.builder()
                    .id(id)
                    .status(SpotConstant.STATUS_ERROR)
                    .errCode(errCode)
                    .errMsg(errMsg)
                    .ts(System.currentTimeMillis())
                    .build());
        } catch (IOException e) {
            log.warn("发送错误响应失败 session={}", session.getId(), e);
        }
    }

    private void send(WebSocketSession session, WsResponse response) throws IOException {
        sendSafe(session, new TextMessage(objectMapper.writeValueAsString(response)));
    }

    /**
     * 线程安全发送：WebSocketSession.sendMessage 非线程安全，
     * 多个 per-symbol executor 线程并发推送同一 session 时需对 session 加锁。
     */
    private void sendSafe(WebSocketSession session, TextMessage msg) throws IOException {
        synchronized (session) {
            session.sendMessage(msg);
        }
    }
}
