package com.nokia.hb.service;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 网络套接字服务
 */
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Service
@Data
@ConfigurationProperties("socket")
public class WebSocketService extends AbstractWebSocketHandler {
    /**
     * 会话映射
     */
    static final ConcurrentMap<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
    /**
     * ping时间map
     */
    static final ConcurrentMap<String, LocalDateTime> PING_MAP = new ConcurrentHashMap<>();
    static final ConcurrentMap<String, AtomicInteger> TIMEOUT_MAP = new ConcurrentHashMap<>();
    /**
     * 心跳超时(s)
     */
    private Long heartbeatTimeout;
    /**
     * 最大心跳超时次数
     */
    private Integer maxHeartbeatTimeout;
    private final SessionService sessionService;

    public WebSocketService(SessionService sessionService) {
        this.sessionService = sessionService;
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        SESSION_MAP.put(session.getId(), session);
        Map<String, Object> attributes = session.getAttributes();
        String sessionId = (String) attributes.get("JSESSIONID");
        String username = (String) attributes.get("username");
        log.info("afterConnectionEstablished: {} -> {} -> {}", session.getId(), sessionId, username);
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        Map<String, Object> attributes = session.getAttributes();
        String sessionId = (String) attributes.get("JSESSIONID");
        String username = (String) attributes.get("username");
        log.error("handleTransportError: {} -> {} -> {} -> {}", session.getId(), sessionId, username,
                exception.getMessage(), exception);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        Map<String, Object> attributes = session.getAttributes();
        String sessionId = (String) attributes.get("JSESSIONID");
        String username = (String) attributes.get("username");
        log.info("afterConnectionClosed: {}, {} -> {} -> {}", closeStatus, session.getId(), sessionId, username);
        sessionService.invalidate(username);
        SESSION_MAP.remove(session.getId());
    }
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        Map<String, Object> attributes = session.getAttributes();
        String sessionId = (String) attributes.get("JSESSIONID");
        String username = (String) attributes.get("username");
        log.info("handleTextMessage: {} -> {} -> {} -> {}", session.getId(), sessionId, username,
                message.getPayload());
    }

    @Override
    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
        Map<String, Object> attributes = session.getAttributes();
        String sessionId = (String) attributes.get("JSESSIONID");
        String username = (String) attributes.get("username");
        log.info("handleBinaryMessage: {} -> {} -> {} -> {}", session.getId(), sessionId, username,
                message.getPayload());
    }

    @Override
    protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
        Map<String, Object> attributes = session.getAttributes();
        String sessionId = (String) attributes.get("JSESSIONID");
        String username = (String) attributes.get("username");
        log.info("handlePongMessage: {} -> {} -> {} -> {}", session.getId(), sessionId, username,
                message.getPayload());
        PING_MAP.put(session.getId(), LocalDateTime.now());
        TIMEOUT_MAP.put(session.getId(), new AtomicInteger(0));
    }

    @Scheduled(fixedDelay = 2000)
    public void ping() {
        for (Map.Entry<String, WebSocketSession> entry : SESSION_MAP.entrySet()) {
            String k = entry.getKey();
            WebSocketSession v = entry.getValue();
            Map<String, Object> attributes = v.getAttributes();
            String sessionId = (String) attributes.get("JSESSIONID");
            String username = (String) attributes.get("username");
            // 发送ping
            try {
                log.info("ping {} -> {} -> {}", v.getId(), sessionId, username);
                v.sendMessage(new PingMessage());
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            }
            LocalDateTime now = LocalDateTime.now();
            LocalDateTime expireTime = now.minusSeconds(heartbeatTimeout);
            PING_MAP.putIfAbsent(k, now);
            TIMEOUT_MAP.putIfAbsent(k, new AtomicInteger(0));
            LocalDateTime pingTime = PING_MAP.get(k);
            boolean isTimeout = expireTime.compareTo(pingTime) > 0;
            log.info("pong: {} -> {} -> {} -> {} -> {} -> {}", v.getId(), sessionId, username, expireTime, pingTime,
                    isTimeout);
            // 心跳超时
            if (isTimeout) {
                log.warn("超过{}s未收到pong: {} -> {} -> {}", heartbeatTimeout, v.getId(), sessionId, username);
                // 超过最大超时次数断开连接
                if (TIMEOUT_MAP.get(k).incrementAndGet() > maxHeartbeatTimeout) {
                    log.warn("超过{}次未收到pong: {} -> {} -> {}", maxHeartbeatTimeout, v.getId(), sessionId, username);
                    try {
                        v.close();
                        PING_MAP.remove(k);
                        TIMEOUT_MAP.remove(k);
                    } catch (IOException e) {
                        log.error(e.getMessage(), e);
                    }
                }
            }
        }
    }
}
