package com.goldenchart.websocket;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.goldenchart.trading.pipeline.TickActivityTracker;
import com.goldenchart.trading.pipeline.TradeEventQueue;
import com.goldenchart.trading.pipeline.TradeTickEvent;
import com.goldenchart.trading.pipeline.TradingPipelineProperties;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 백엔드 업비트 WebSocket 클라이언트 (명세서 3.1).
 *
 * - 고정 관심종목(최대 20개) + 동적 차트 오픈 종목(최대 8개) 구독
 * - 수신한 trade 틱을 BarBuilder 로 전달하여 캔들 조립
 * - 연결 끊김 시 지수 백오프 재연결 (최대 60초 간격)
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class UpbitWebSocketClient {

    private final BarBuilder barBuilder;
    private final TradeEventQueue tradeEventQueue;
    private final TradingPipelineProperties pipelineProperties;
    private final TickActivityTracker activityTracker;
    private final ObjectMapper objectMapper;

    private final AtomicLong messagesReceived = new AtomicLong();
    private final AtomicLong parseErrors = new AtomicLong();
    private volatile long lastMessageAtMs;
    private volatile long connectedAtMs;
    private volatile int lastCloseCode;
    private volatile String lastCloseReason = "";

    @Value("${upbit.websocket.url:wss://api.upbit.com/websocket/v1}")
    private String wsUrl;

    /** 현재 구독 중인 마켓 코드 집합 (thread-safe) */
    private final CopyOnWriteArraySet<String> subscribedMarkets = new CopyOnWriteArraySet<>();

    private volatile InternalWsClient wsClient;
    private final AtomicBoolean running = new AtomicBoolean(false);

    private final ScheduledExecutorService scheduler =
            Executors.newSingleThreadScheduledExecutor(r -> {
                Thread t = new Thread(r, "upbit-ws-reconnect");
                t.setDaemon(true);
                return t;
            });

    private int reconnectDelaySec = 2;
    private static final int MAX_RECONNECT_DELAY = 60;

    // ── 생명주기 ──────────────────────────────────────────────────────────────

    @PostConstruct
    public void init() {
        running.set(true);
        connect();
    }

    @PreDestroy
    public void destroy() {
        running.set(false);
        scheduler.shutdownNow();
        if (wsClient != null) wsClient.close();
        log.info("[UpbitWebSocketClient] 종료");
    }

    // ── 구독 관리 (DynamicSubscriptionManager 에서 호출) ──────────────────────

    /**
     * 마켓을 구독 목록에 추가하고 웹소켓 재구독 패킷 전송.
     */
    public synchronized void addMarket(String market) {
        if (subscribedMarkets.add(market)) {
            log.info("[UpbitWebSocketClient] 마켓 추가: {} (총 {})", market, subscribedMarkets.size());
            sendSubscribePacket();
        } else if (wsClient != null && wsClient.isOpen()) {
            // 이미 목록에 있어도 재연결 직후 구독 패킷이 누락될 수 있음
            sendSubscribePacket();
        }
    }

    /**
     * 마켓을 구독 목록에서 제거하고 웹소켓 재구독 패킷 전송.
     */
    public synchronized void removeMarket(String market) {
        if (subscribedMarkets.remove(market)) {
            log.info("[UpbitWebSocketClient] 마켓 제거: {}", market);
            sendSubscribePacket();
        }
    }

    public Set<String> getSubscribedMarkets() {
        return Collections.unmodifiableSet(subscribedMarkets);
    }

    public boolean isConnected() {
        return wsClient != null && wsClient.isOpen();
    }

    public Map<String, Object> monitorSnapshot() {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("connected", isConnected());
        m.put("subscribedMarkets", subscribedMarkets.size());
        m.put("messagesReceived", messagesReceived.get());
        m.put("parseErrors", parseErrors.get());
        m.put("lastMessageAtMs", lastMessageAtMs);
        m.put("connectedAtMs", connectedAtMs);
        m.put("lastCloseCode", lastCloseCode);
        m.put("lastCloseReason", lastCloseReason);
        m.put("reconnectDelaySec", reconnectDelaySec);
        return m;
    }

    /** 기동·STOMP 구독 직후 — 업비트에 구독 패킷 재전송 */
    public void resubscribeAll() {
        sendSubscribePacket();
    }

    /** 틱 정체 등 — 외부 모니터가 재연결을 요청 */
    public void requestReconnect(String reason) {
        log.warn("[UpbitWebSocketClient] 재연결 요청: {}", reason);
        if (wsClient != null && wsClient.isOpen()) {
            wsClient.close();
        } else if (running.get()) {
            scheduleReconnect();
        }
    }

    // ── 내부 연결 / 재연결 ────────────────────────────────────────────────────

    private void connect() {
        if (!running.get()) return;
        try {
            wsClient = new InternalWsClient(new URI(wsUrl));
            wsClient.connect();
        } catch (Exception e) {
            log.error("[UpbitWebSocketClient] 연결 실패: {}", e.getMessage());
            scheduleReconnect();
        }
    }

    private void scheduleReconnect() {
        if (!running.get()) return;
        log.info("[UpbitWebSocketClient] {}초 후 재연결 시도...", reconnectDelaySec);
        scheduler.schedule(() -> {
            connect();
            reconnectDelaySec = Math.min(reconnectDelaySec * 2, MAX_RECONNECT_DELAY);
        }, reconnectDelaySec, TimeUnit.SECONDS);
    }

    /** 현재 subscribedMarkets 기반으로 업비트 구독 JSON 패킷 전송 */
    private void sendSubscribePacket() {
        if (wsClient == null || !wsClient.isOpen() || subscribedMarkets.isEmpty()) return;
        try {
            List<String> codes = new ArrayList<>(subscribedMarkets);
            // 업비트 WebSocket 구독 포맷
            String packet = objectMapper.writeValueAsString(List.of(
                    Map.of("ticket", UUID.randomUUID().toString()),
                    Map.of("type", "trade", "codes", codes, "isOnlyRealtime", true),
                    Map.of("format", "SIMPLE")
            ));
            wsClient.send(packet);
            log.debug("[UpbitWebSocketClient] 구독 패킷 전송: {}", codes);
        } catch (Exception e) {
            log.error("[UpbitWebSocketClient] 구독 패킷 전송 실패: {}", e.getMessage());
        }
    }

    // ── 내부 WebSocket 구현 ───────────────────────────────────────────────────

    private class InternalWsClient extends WebSocketClient {

        InternalWsClient(URI uri) {
            super(uri);
        }

        @Override
        public void onOpen(ServerHandshake handshake) {
            log.info("[UpbitWebSocketClient] 연결 성공");
            reconnectDelaySec = 2;  // 재연결 딜레이 리셋
            connectedAtMs = System.currentTimeMillis();
            sendSubscribePacket();
        }

        @Override
        public void onMessage(String message) {
            // 업비트는 binary frame 으로 전송 — text fallback 무시
        }

        @Override
        public void onMessage(java.nio.ByteBuffer bytes) {
            try {
                byte[] payload = new byte[bytes.remaining()];
                bytes.get(payload);
                JsonNode node = objectMapper.readTree(payload);

                String ty = node.path("ty").asText("");
                if (ty.isEmpty()) ty = node.path("type").asText("");
                if (!"trade".equals(ty)) return;

                messagesReceived.incrementAndGet();
                lastMessageAtMs = System.currentTimeMillis();

                String market = node.path("cd").asText("");
                if (market.isEmpty()) market = node.path("code").asText("");
                if (market.isEmpty()) return;

                double tradePrice = node.path("tp").asDouble();
                if (tradePrice == 0) tradePrice = node.path("trade_price").asDouble();
                double tradeVolume = node.path("tv").asDouble();
                if (tradeVolume == 0) tradeVolume = node.path("trade_volume").asDouble();

                long tradeTimeMs = node.path("tms").asLong();
                if (tradeTimeMs == 0) tradeTimeMs = node.path("ttms").asLong();
                if (tradeTimeMs == 0) tradeTimeMs = node.path("trade_timestamp").asLong();
                if (tradeTimeMs == 0) tradeTimeMs = node.path("timestamp").asLong();
                if (tradeTimeMs == 0) tradeTimeMs = System.currentTimeMillis();

                activityTracker.record(market);
                if (pipelineProperties.isEnabled()) {
                    tradeEventQueue.offer(TradeTickEvent.of(
                            market, tradePrice, tradeVolume, tradeTimeMs));
                } else {
                    barBuilder.onTick(market, tradePrice, tradeVolume, tradeTimeMs);
                }
            } catch (Exception e) {
                parseErrors.incrementAndGet();
                log.warn("[UpbitWebSocketClient] 메시지 파싱 오류: {}", e.getMessage());
            }
        }

        @Override
        public void onClose(int code, String reason, boolean remote) {
            lastCloseCode = code;
            lastCloseReason = reason != null ? reason : "";
            log.warn("[UpbitWebSocketClient] 연결 종료 code={}, reason={}", code, reason);
            if (running.get()) scheduleReconnect();
        }

        @Override
        public void onError(Exception ex) {
            log.error("[UpbitWebSocketClient] 에러: {}", ex.getMessage());
        }
    }
}
