package com.ttstd.signaling.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ttstd.signaling.manager.ConnectionRequestManager;
import com.ttstd.signaling.manager.SessionManager;
import com.ttstd.signaling.manager.SignalMetrics;
import com.ttstd.signaling.model.AuthPrincipal;
import com.ttstd.signaling.model.DeviceBinding;
import com.ttstd.signaling.model.DeviceType;
import com.ttstd.signaling.model.SignalMessage;
import com.ttstd.signaling.security.AuthHandshakeInterceptor;
import com.ttstd.signaling.service.AccountService;
import com.ttstd.signaling.service.AuditService;
import com.ttstd.signaling.service.BindingService;
import com.ttstd.signaling.service.DeviceIdentityService;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Component
public class SignalWebSocketHandler extends TextWebSocketHandler {

    private static final Logger logger = LoggerFactory.getLogger(SignalWebSocketHandler.class);
    private static final ObjectMapper objectMapper = new ObjectMapper();

    /** 因鉴权/封禁被关闭连接时使用的关闭码 */
    private static final CloseStatus CLOSE_UNAUTHORIZED = new CloseStatus(4001, "UNAUTHORIZED");
    private static final CloseStatus CLOSE_FORCED_LOGOUT = new CloseStatus(4003, "FORCED_LOGOUT");

    private final SessionManager sessionManager;
    private final ConnectionRequestManager connectionRequestManager;
    private final SignalMetrics metrics;
    private final AccountService accountService;
    private final DeviceIdentityService deviceIdentityService;
    private final BindingService bindingService;
    private final AuditService auditService;

    public SignalWebSocketHandler(SessionManager sessionManager,
                                  ConnectionRequestManager connectionRequestManager,
                                  SignalMetrics metrics,
                                  AccountService accountService,
                                  DeviceIdentityService deviceIdentityService,
                                  BindingService bindingService,
                                  AuditService auditService) {
        this.sessionManager = sessionManager;
        this.connectionRequestManager = connectionRequestManager;
        this.metrics = metrics;
        this.accountService = accountService;
        this.deviceIdentityService = deviceIdentityService;
        this.bindingService = bindingService;
        this.auditService = auditService;
        this.connectionRequestManager.setSender(this::sendToDevice);
    }

    /**
     * 注册会话失效回调：账号被封禁 / 会话被踢出 / 设备被禁用时立即断开对应连接。
     */
    @PostConstruct
    void registerRevocationListener() {
        AccountService.SessionRevocationListener listener = this::forceDisconnect;
        accountService.setRevocationListener(listener);
        deviceIdentityService.setRevocationListener(listener);
    }

    /**
     * 强制断开指定主体的连接。
     *
     * @param principalId 主体 ID
     * @param sessionId   指定登录会话；为 null 表示断开该主体全部连接
     */
    private void forceDisconnect(String principalId, String sessionId, String reason) {
        Set<String> deviceIds = sessionManager.getDeviceIdsByPrincipal(principalId);
        for (String deviceId : deviceIds) {
            AuthPrincipal principal = sessionManager.getPrincipal(deviceId);
            if (principal == null) {
                continue;
            }
            // 指定了会话时只断开该会话，避免误伤同账号其他端
            if (sessionId != null && !sessionId.equals(principal.sessionId())) {
                continue;
            }
            WebSocketSession session = sessionManager.getSession(deviceId);
            if (session == null || !session.isOpen()) {
                continue;
            }
            Map<String, Object> notice = new HashMap<>();
            notice.put("type", "FORCE_LOGOUT");
            notice.put("payload", reason == null ? "会话已失效" : reason);
            sendToSession(session, notice);
            try {
                session.close(CLOSE_FORCED_LOGOUT);
            } catch (IOException e) {
                logger.warn("强制断开连接 {} 失败: {}", deviceId, e.getMessage());
            }
            logger.info("已强制断开连接: deviceId={} principal={} reason={}",
                    deviceId, principalId, reason);
        }
    }

    /**
     * 向指定设备回送服务端事件（错误/超时通知等）。
     */
    private void sendToDevice(String deviceId, Object message) {
        WebSocketSession session = sessionManager.getSession(deviceId);
        if (session == null || !session.isOpen()) {
            logger.warn("无法向 {} 发送事件：连接不在线", deviceId);
            return;
        }
        sendToSession(session, message);
    }

    /**
     * 连接建立后立即以握手阶段裁定的身份完成注册，无需客户端再发 REGISTER。
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        AuthPrincipal principal = principalOf(session);
        if (principal == null) {
            // 正常情况下握手拦截器已拦截，此处为纵深防御
            logger.warn("连接 {} 缺少鉴权主体，立即关闭", session.getId());
            session.close(CLOSE_UNAUTHORIZED);
            return;
        }

        sessionManager.registerDevice(principal, session);
        metrics.incRegistered();
        metrics.recordSessionCount(sessionManager.getOnlineCount());

        Map<String, Object> response = new HashMap<>();
        response.put("type", "REGISTER_SUCCESS");
        response.put("deviceId", principal.deviceId());
        // 与既有信令消息协议一致：下发的设备ID也写入 fromDeviceId，便于客户端统一解析。
        response.put("fromDeviceId", principal.deviceId());
        response.put("deviceType", principal.deviceType().name());
        response.put("displayName", principal.displayName());
        sendToSession(session, response);

        logger.info("连接已建立并注册: {} ({})", principal.deviceId(), principal.deviceType());
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        AuthPrincipal principal = principalOf(session);
        if (principal == null) {
            session.close(CLOSE_UNAUTHORIZED);
            return;
        }

        String payload = message.getPayload();
        logger.debug("收到消息: {}", payload);
        metrics.incMessage();

        try {
            SignalMessage signalMessage = objectMapper.readValue(payload, SignalMessage.class);
            String type = signalMessage.getType();

            if (type == null) {
                logger.warn("消息缺少 type 字段");
                return;
            }

            // 关键：发送方身份一律以服务端鉴权结果覆盖，忽略客户端自报值，
            // 防止伪造 fromDeviceId 冒充他人。
            String claimedFrom = signalMessage.getFromDeviceId();
            if (claimedFrom != null && !claimedFrom.equals(principal.deviceId())) {
                logger.warn("客户端自报 fromDeviceId={} 与鉴权身份 {} 不一致，已强制覆盖",
                        claimedFrom, principal.deviceId());
            }
            signalMessage.setFromDeviceId(principal.deviceId());
            signalMessage.setDeviceType(principal.deviceType().name());

            switch (type.toUpperCase()) {
                case "REGISTER":
                    // 身份已在握手阶段确定，REGISTER 仅作兼容响应
                    handleLegacyRegister(session, principal);
                    break;
                case "DEVICE_LIST":
                    handleDeviceList(session, principal);
                    break;
                case "PING":
                    // 客户端心跳保活消息，无需处理，仅用于防止中间代理因空闲超时断开连接
                    break;
                case "OFFER":
                    metrics.incOffer();
                    // 绑定/黑名单前置校验：仅允许已绑定且未被拉黑的主控端发起
                    if (principal.deviceType() == DeviceType.CONTROLLER) {
                        String targetUid = signalMessage.getToDeviceId();
                        if (targetUid == null || targetUid.isEmpty()) {
                            sendError(session, "缺少目标设备ID");
                            metrics.incBlockedOffer();
                            break;
                        }
                        if (!bindingService.isBound(targetUid, principal.principalId())) {
                            sendError(session, "未与该设备建立绑定关系，无法发起连接");
                            metrics.incBlockedOffer();
                            auditOfferBlocked(targetUid, principal.principalId(), "NOT_BOUND");
                            break;
                        }
                        if (bindingService.isBlacklisted(targetUid, principal.principalId())) {
                            sendError(session, "该设备已拒绝来自你的连接");
                            metrics.incBlockedOffer();
                            auditOfferBlocked(targetUid, principal.principalId(), "BLACKLISTED");
                            break;
                        }
                    }
                    // 连接请求：统一经 ConnectionRequestManager 做校验/去重/待确认跟踪后再转发
                    handleConnectionRequest(signalMessage);
                    break;
                case "ANSWER":
                    metrics.incAnswer();
                    // 被控端已接受：清理待确认状态并转发 Answer 给主控端
                    connectionRequestManager.completePendingRequest(
                            signalMessage.getToDeviceId(), signalMessage.getFromDeviceId());
                    forwardMessage(signalMessage, true);
                    break;
                case "CONNECTION_REJECTED":
                    metrics.incRejected();
                    // 被控端已拒绝：清理待确认状态并转发拒绝通知给主控端
                    connectionRequestManager.completePendingRequest(
                            signalMessage.getToDeviceId(), signalMessage.getFromDeviceId());
                    forwardMessage(signalMessage, false);
                    break;
                case "ICE_CANDIDATE":
                case "CONTROL_COMMAND":
                    // 普通信令：目标不在线时静默丢弃，不返回提示
                    forwardMessage(signalMessage, false);
                    break;
                default:
                    // 其他消息类型直接转发，目标不在线时静默丢弃
                    forwardMessage(signalMessage, false);
                    break;
            }
        } catch (Exception e) {
            logger.error("处理消息出错: {}", e.getMessage(), e);
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        logger.info("连接已关闭: {} ({})", session.getId(), status);
        sessionManager.unregisterSession(session);
        metrics.recordSessionCount(sessionManager.getOnlineCount());
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // EOFException / 连接被对端重置 属于正常网络事件（客户端网络中断、代理空闲超时等），
        // 并非服务器错误，降级为 DEBUG 记录，避免误导。
        boolean benign = exception instanceof java.io.EOFException
                || (exception.getMessage() != null
                    && (exception.getMessage().contains("Connection reset")
                        || exception.getMessage().contains("Broken pipe")
                        || exception.getMessage().contains("An established connection")));
        if (benign) {
            logger.debug("传输层关闭（客户端断开），会话 {}: {}",
                    session.getId(), exception.getMessage());
        } else {
            logger.warn("传输层错误，会话 {}: {}", session.getId(), exception.getMessage());
        }
        sessionManager.unregisterSession(session);
        metrics.recordSessionCount(sessionManager.getOnlineCount());
        // 连接已不可用时无需再以 SERVER_ERROR(1011) 关闭；仅在仍打开时温和关闭。
        if (session.isOpen()) {
            session.close(CloseStatus.GOING_AWAY);
        }
    }

    private AuthPrincipal principalOf(WebSocketSession session) {
        Object attr = session.getAttributes().get(AuthHandshakeInterceptor.ATTR_PRINCIPAL);
        return attr instanceof AuthPrincipal principal ? principal : null;
    }

    /**
     * 兼容旧客户端的 REGISTER：不再接受客户端自报身份，仅回显服务端裁定结果。
     */
    private void handleLegacyRegister(WebSocketSession session, AuthPrincipal principal) {
        Map<String, Object> response = new HashMap<>();
        response.put("type", "REGISTER_SUCCESS");
        response.put("deviceId", principal.deviceId());
        response.put("fromDeviceId", principal.deviceId());
        response.put("deviceType", principal.deviceType().name());
        response.put("displayName", principal.displayName());
        sendToSession(session, response);
    }

    /**
     * 设备列表（绑定视图）。
     *
     * <p>为防止枚举被控端，服务端不再向任意主控端返回全局在线设备清单。
     * 主控端仅能看到「自己已绑定」的设备及其在线状态；被控端此项为空列表。
     */
    private void handleDeviceList(WebSocketSession session, AuthPrincipal principal) {
        Map<String, Object> response = new HashMap<>();
        response.put("type", "DEVICE_LIST");

        if (principal.deviceType() == DeviceType.CONTROLLER) {
            List<Map<String, Object>> devices = bindingService.listByUser(principal.principalId())
                    .stream()
                    .filter(b -> b.getStatus() == DeviceBinding.BindingStatus.ACTIVE)
                    .map(b -> {
                        Map<String, Object> item = new HashMap<>();
                        item.put("deviceUid", b.getDeviceUid());
                        item.put("alias", b.getAlias());
                        item.put("role", b.getRole().name());
                        item.put("online", sessionManager.isDeviceOnline(b.getDeviceUid()));
                        return item;
                    })
                    .toList();
            response.put("boundDevices", devices);
        } else {
            // 被控端无需浏览主控端列表
            response.put("boundDevices", List.of());
        }
        sendToSession(session, response);
    }

    /** 记录被拦截的 OFFER（未绑定/被拉黑），便于审计与风控。 */
    private void auditOfferBlocked(String deviceUid, String userId, String reason) {
        try {
            auditService.recordUser(userId, AuditService.ACTION_OFFER_BLOCKED,
                    AuditService.RESULT_SUCCESS, null, "device=" + deviceUid + " reason=" + reason);
        } catch (Exception e) {
            logger.warn("审计 OFFER 拦截记录失败: {}", e.getMessage());
        }
    }

    /**
     * 处理主控端发来的连接请求（OFFER）：先做设备类型校验与去重，
     * 再登记“待被控端确认”状态，最后转发给被控端。
     */
    private void handleConnectionRequest(SignalMessage message) {
        String fromDeviceId = message.getFromDeviceId();
        String toDeviceId = message.getToDeviceId();

        // 1. 校验：仅允许 CONTROLLER -> CONTROLLED
        String error = connectionRequestManager.validateOffer(fromDeviceId, toDeviceId);
        if (error != null) {
            logger.warn("非法连接请求 {} -> {}: {}", fromDeviceId, toDeviceId, error);
            Map<String, Object> response = new HashMap<>();
            response.put("type", "REQUEST_ERROR");
            response.put("toDeviceId", toDeviceId);
            response.put("payload", error);
            sendToDevice(fromDeviceId, response);
            return;
        }

        // 鉴权类型分类：动态验证码(CODE) / 固定密码(PASSWORD) / 免密(NONE)。
        // 服务器只做分类与转发，真实校验在被控端完成（服务器不保存任何密钥）。
        String authType = message.getAuthType();
        if (authType != null && !authType.trim().isEmpty()) {
            if ("CODE".equalsIgnoreCase(authType)) {
                logger.info("连接请求 {} -> {} 使用【动态验证码】鉴权", fromDeviceId, toDeviceId);
            } else if ("PASSWORD".equalsIgnoreCase(authType)) {
                logger.info("连接请求 {} -> {} 使用【固定密码】鉴权", fromDeviceId, toDeviceId);
            } else if ("NONE".equalsIgnoreCase(authType)) {
                // 显式标记为免密，便于被控端识别
                message.setAuthType("NONE");
                logger.info("连接请求 {} -> {} 使用【免密连接】鉴权（被控端手动确认）", fromDeviceId, toDeviceId);
            } else {
                logger.warn("连接请求 {} -> {} 携带未知鉴权类型 authType={}", fromDeviceId, toDeviceId, authType);
            }
        } else {
            // 未携带 authType 也视为免密连接（向后兼容），规范化为 NONE
            message.setAuthType("NONE");
            logger.info("连接请求 {} -> {} 未携带鉴权，按【免密连接】处理（被控端手动确认）", fromDeviceId, toDeviceId);
        }

        // 2. 去重：短时间内重复 OFFER 直接忽略，避免被控端反复弹窗
        if (connectionRequestManager.isDuplicateOffer(fromDeviceId, toDeviceId)) {
            logger.info("重复连接请求 {} -> {} 已忽略", fromDeviceId, toDeviceId);
            return;
        }

        // 3. 目标不在线：立即回送 TARGET_OFFLINE，且不登记待确认（避免等待超时）
        if (!sessionManager.isDeviceOnline(toDeviceId)) {
            logger.warn("目标被控端 {} 不在线，无法投递连接请求", toDeviceId);
            notifySenderTargetOffline(message, toDeviceId);
            return;
        }

        // 4. 登记待确认并转发 OFFER 给被控端
        connectionRequestManager.registerPendingRequest(fromDeviceId, toDeviceId);
        forwardMessage(message, false);
    }

    private void forwardMessage(SignalMessage message, boolean notifyOffline) {
        String toDeviceId = message.getToDeviceId();
        if (toDeviceId == null) {
            logger.warn("无法转发消息：toDeviceId 为空");
            return;
        }

        WebSocketSession targetSession = sessionManager.getSession(toDeviceId);
        if (targetSession == null || !targetSession.isOpen()) {
            logger.warn("目标设备 {} 不在线", toDeviceId);
            // 仅 OFFER / ANSWER 在目标不在线时回送 TARGET_OFFLINE，其余类型静默丢弃
            if (notifyOffline) {
                notifySenderTargetOffline(message, toDeviceId);
            }
            return;
        }

        try {
            String jsonMessage = objectMapper.writeValueAsString(message);
            targetSession.sendMessage(new TextMessage(jsonMessage));
            logger.debug("已转发 {}：{} -> {}", message.getType(), message.getFromDeviceId(), toDeviceId);
        } catch (IOException e) {
            logger.error("转发消息到 {} 失败: {}", toDeviceId, e.getMessage());
        }
    }

    /**
     * 目标被控端不在线时，向发送方（主控端）回送 TARGET_OFFLINE 提醒。
     */
    private void notifySenderTargetOffline(SignalMessage message, String offlineDeviceId) {
        String fromDeviceId = message.getFromDeviceId();
        if (fromDeviceId == null) {
            logger.warn("无法通知离线状态：fromDeviceId 为空");
            return;
        }

        WebSocketSession senderSession = sessionManager.getSession(fromDeviceId);
        if (senderSession == null || !senderSession.isOpen()) {
            logger.warn("发送方 {} 会话不存在，无法通知目标离线", fromDeviceId);
            return;
        }

        Map<String, Object> response = new HashMap<>();
        response.put("type", "TARGET_OFFLINE");
        response.put("toDeviceId", offlineDeviceId);
        response.put("payload", "目标被控端不在线，请确认设备已开启并连接到信令服务器");

        sendToSession(senderSession, response);
        logger.info("已通知发送方 {} 目标 {} 不在线", fromDeviceId, offlineDeviceId);
    }

    private void sendToSession(WebSocketSession session, Object data) {
        try {
            String json = objectMapper.writeValueAsString(data);
            session.sendMessage(new TextMessage(json));
        } catch (IOException e) {
            logger.error("向会话 {} 发送消息失败: {}", session.getId(), e.getMessage());
        }
    }

    /** 向发送方回送一条 REQUEST_ERROR 业务提示（不关闭连接）。 */
    private void sendError(WebSocketSession session, String message) {
        Map<String, Object> response = new HashMap<>();
        response.put("type", "REQUEST_ERROR");
        response.put("payload", message);
        sendToSession(session, response);
    }
}
