package com.market.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.market.manager.SpotClientSessionManager;
import com.market.model.SpotClientSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 现货行情推送服务（直接推送，无 MQ 中转）
 * <p>
 * 维护 topic→sessionId 双向映射，Binance 数据到达后直接推送给订阅该 topic 的前端 session。
 * <p>
 * topic 格式：
 * <pre>
 *   market_{symbol}_kline_{flutterPeriod}   如 market_btcusdt_kline_1min
 *   market_{symbol}_trade_ticker
 *   market_{symbol}_ticker
 *   market_{symbol}_depth
 * </pre>
 *
 * @author system
 * @date 2026-05-07
 */
@Service
public class SpotMarketPushService {

    private static final Logger log = LoggerFactory.getLogger(SpotMarketPushService.class);

    private final SpotClientSessionManager sessionManager;
    private final ObjectMapper objectMapper = new ObjectMapper();

    /** topic → Set&lt;clientId&gt; */
    private final ConcurrentHashMap<String, Set<String>> topicClients = new ConcurrentHashMap<>();
    /** clientId → Set&lt;topic&gt;（断线快速清理） */
    private final ConcurrentHashMap<String, Set<String>> clientTopics = new ConcurrentHashMap<>();
    /** topic → 最新数据缓存（订阅后立即推送） */
    private final ConcurrentHashMap<String, Object> latestCache = new ConcurrentHashMap<>();

    public SpotMarketPushService(SpotClientSessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    // ------------------------------------------------------------------ 订阅管理

    /**
     * 订阅 topic
     *
     * @param clientId 客户端ID
     * @param topic    topic名
     */
    public void subscribe(String clientId, String topic) {
        topicClients.computeIfAbsent(topic, k -> ConcurrentHashMap.newKeySet()).add(clientId);
        clientTopics.computeIfAbsent(clientId, k -> ConcurrentHashMap.newKeySet()).add(topic);
    }

    /**
     * 取消订阅 topic
     *
     * @param clientId 客户端ID
     * @param topic    topic名
     */
    public void unsubscribe(String clientId, String topic) {
        Set<String> clients = topicClients.get(topic);
        if (clients != null) {
            clients.remove(clientId);
        }
        Set<String> topics = clientTopics.get(clientId);
        if (topics != null) {
            topics.remove(topic);
        }
    }

    /**
     * 移除客户端所有订阅（断线时调用）
     *
     * @param clientId 客户端ID
     */
    public void removeClient(String clientId) {
        Set<String> topics = clientTopics.remove(clientId);
        if (topics != null) {
            for (String topic : topics) {
                Set<String> clients = topicClients.get(topic);
                if (clients != null) {
                    clients.remove(clientId);
                }
            }
        }
    }

    // ------------------------------------------------------------------ 推送

    /**
     * 推送 tick 数据到订阅该 topic 的所有 session，并更新最新缓存。
     *
     * @param topic topic名
     * @param tick  tick 数据（将包装在 channel/ts/tick 结构中推送）
     */
    public void push(String topic, Object tick) {
        // 1. 缓存最新数据
        latestCache.put(topic, tick);

        // 2. 无订阅者则跳过序列化
        Set<String> clients = topicClients.get(topic);
        if (clients == null || clients.isEmpty()) {
            return;
        }

        // 3. 序列化一次，直接推送给所有订阅者（参照合约 FrontendWebSocketHandler.push，不经过队列）
        TextMessage msg;
        try {
            Map<String, Object> resp = new java.util.HashMap<>();
            resp.put("channel", topic);
            resp.put("ts", System.currentTimeMillis());
            resp.put("event_rep", "sub");
            resp.put("status", "ok");
            resp.put("event", "sub");
            resp.put("tick", tick);
            msg = new TextMessage(objectMapper.writeValueAsString(resp));
        } catch (Exception e) {
            log.warn("序列化推送消息失败: topic={}", topic, e);
            return;
        }

        for (String clientId : clients) {
            SpotClientSession clientSession = sessionManager.getSession(clientId);
            if (clientSession != null && clientSession.getSession().isOpen()) {
                try {
                    sendSafe(clientSession, msg);
                } catch (Exception e) {
                    log.warn("现货推送失败: topic={}, clientId={}", topic, clientId, e);
                }
            }
        }
    }

    /**
     * 获取 topic 最新缓存（订阅后立即返回）
     *
     * @param topic topic名
     * @return 最新数据，无缓存返回 null
     */
    public Object getLatest(String topic) {
        return latestCache.get(topic);
    }

    /**
     * 向单个 session 直接发送 topic 数据（订阅后立即推送历史缓存）
     *
     * @param session 目标会话
     * @param topic   topic名
     * @param tick    tick 数据
     */
    public void pushToSession(SpotClientSession session, String topic, Object tick) {
        try {
            Map<String, Object> resp = new java.util.HashMap<>();
            resp.put("channel", topic);
            resp.put("ts", System.currentTimeMillis());
            resp.put("event_rep", "sub");
            resp.put("status", "ok");
            resp.put("event", "sub");
            resp.put("tick", tick);
            if (session.getSession().isOpen()) {
                sendSafe(session, new TextMessage(objectMapper.writeValueAsString(resp)));
            }
        } catch (Exception e) {
            log.warn("立即推送失败: topic={}, clientId={}", topic, session.getId(), e);
        }
    }

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