package server.logic.ws_protocol.JSON.messages;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallSignalToSession_Response;
import server.logic.ws_protocol.JSON.push.WebPushSender;
import server.logic.ws_protocol.JSON.push.WsEventSender;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class Net_CallSignalToSession_Handler implements JsonMessageHandler {
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final int TYPE_ACCEPT = 120;
    private static final int TYPE_DECLINE_BUSY = 130;
    private static final int TYPE_TIMEOUT = 140;
    private static final int TYPE_HANGUP = 150;

    @Override
    public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
        Net_CallSignalToSession_Request req = (Net_CallSignalToSession_Request) baseRequest;
        if (ctx == null || !ctx.isAuthenticatedUser()) {
            return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
        }

        String toRequest = req.getToLogin() == null ? "" : req.getToLogin().trim();
        String targetSessionId = req.getTargetSessionId() == null ? "" : req.getTargetSessionId().trim();
        String callId = req.getCallId() == null ? "" : req.getCallId().trim();
        Integer type = req.getType();
        String data = req.getData() == null ? "" : req.getData();

        if (toRequest.isBlank() || targetSessionId.isBlank() || callId.isBlank() || type == null) {
            return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin/targetSessionId/callId/type обязательны");
        }

        SolanaUserEntry targetUser = SolanaUsersDAO.getInstance().getByLogin(toRequest);
        if (targetUser == null) {
            return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "Пользователь не найден");
        }
        String to = targetUser.getLogin();

        ConnectionContext targetCtx = ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);
        if (targetCtx == null || !to.equalsIgnoreCase(targetCtx.getLogin())) {
            // Fallback: если точной сессии уже нет (переподключение), но у пользователя ровно 1 активная сессия,
            // отправляем в неё, чтобы не ронять звонок из-за устаревшего targetSessionId.
            Set<ConnectionContext> activeForLogin = ActiveConnectionsRegistry.getInstance().getByLogin(to);
            if (activeForLogin.size() == 1) {
                targetCtx = activeForLogin.iterator().next();
            } else {
                return NetExceptionResponseFactory.error(req, 404, "SESSION_NOT_FOUND", "Целевая сессия не найдена");
            }
        }

        String eventId = NetIdGenerator.eventId("evt");
        ObjectNode payload = MAPPER.createObjectNode();
        payload.put("eventId", eventId);
        payload.put("fromLogin", ctx.getLogin());
        payload.put("fromSessionId", ctx.getSessionId());
        payload.put("toLogin", to);
        payload.put("callId", callId);
        payload.put("type", type);
        payload.put("data", data);
        payload.put("timeMs", System.currentTimeMillis());

        boolean delivered = WsEventSender.sendEvent(targetCtx, "IncomingCallSignal", eventId, payload);

        if (type == TYPE_ACCEPT) {
            notifyStopOnOtherSessions(
                    ctx.getLogin(),
                    ctx.getSessionId(),
                    ctx.getLogin(),
                    ctx.getSessionId(),
                    callId,
                    "accepted_on_other_device"
            );
        }

        if (type == TYPE_DECLINE_BUSY || type == TYPE_TIMEOUT || type == TYPE_HANGUP) {
            String reason = "terminal_call_signal_" + type;
            notifyStopOnOtherSessions(
                    ctx.getLogin(),
                    ctx.getSessionId(),
                    ctx.getLogin(),
                    ctx.getSessionId(),
                    callId,
                    reason
            );
            notifyStopOnOtherSessions(
                    to,
                    targetCtx.getSessionId(),
                    ctx.getLogin(),
                    ctx.getSessionId(),
                    callId,
                    reason
            );
        }

        Net_CallSignalToSession_Response resp = new Net_CallSignalToSession_Response();
        resp.setOp(req.getOp());
        resp.setRequestId(req.getRequestId());
        resp.setStatus(delivered ? WireCodes.Status.OK : 404);
        resp.setDelivered(delivered);
        return resp;
    }

    private void notifyStopOnOtherSessions(
            String targetLogin,
            String excludeSessionId,
            String fromLogin,
            String fromSessionId,
            String callId,
            String reason
    ) throws Exception {
        if (isBlank(targetLogin) || isBlank(callId)) {
            return;
        }

        Set<String> onlineSessionIds = new HashSet<>();
        Set<ConnectionContext> sameUserSessions = ActiveConnectionsRegistry.getInstance().getByLogin(targetLogin);
        for (ConnectionContext siblingCtx : sameUserSessions) {
            if (siblingCtx == null || siblingCtx.getWsSession() == null || !siblingCtx.getWsSession().isOpen()) continue;
            onlineSessionIds.add(String.valueOf(siblingCtx.getSessionId() == null ? "" : siblingCtx.getSessionId()).trim());
            if (!isBlank(excludeSessionId) && excludeSessionId.equals(siblingCtx.getSessionId())) continue;

            String siblingEventId = NetIdGenerator.eventId("evt");
            ObjectNode siblingPayload = MAPPER.createObjectNode();
            siblingPayload.put("eventId", siblingEventId);
            siblingPayload.put("fromLogin", fromLogin);
            siblingPayload.put("fromSessionId", fromSessionId);
            siblingPayload.put("toLogin", targetLogin);
            siblingPayload.put("callId", callId);
            siblingPayload.put("type", TYPE_HANGUP);
            siblingPayload.put("data", reason);
            siblingPayload.put("timeMs", System.currentTimeMillis());

            WsEventSender.sendEvent(siblingCtx, "IncomingCallSignal", siblingEventId, siblingPayload);
        }

        List<ActiveSessionEntry> persistedSessions = ActiveSessionsDAO.getInstance().getByLogin(targetLogin);
        long sentAtMs = System.currentTimeMillis();
        for (ActiveSessionEntry session : persistedSessions) {
            String sessionId = String.valueOf(session.getSessionId() == null ? "" : session.getSessionId()).trim();
            if (!isBlank(excludeSessionId) && excludeSessionId.equals(sessionId)) continue;
            if (!sessionId.isBlank() && onlineSessionIds.contains(sessionId)) continue;
            if (isBlank(session.getPushEndpoint()) || isBlank(session.getPushP256dhKey()) || isBlank(session.getPushAuthKey())) {
                continue;
            }
            String pushPayload = "{\"kind\":\"stop_call\""
                    + ",\"callId\":\"" + jsonEscape(callId) + "\""
                    + ",\"reason\":\"" + jsonEscape(reason) + "\""
                    + ",\"fromLogin\":\"" + jsonEscape(fromLogin) + "\""
                    + ",\"fromSessionId\":\"" + jsonEscape(fromSessionId) + "\""
                    + ",\"toLogin\":\"" + jsonEscape(targetLogin) + "\""
                    + ",\"sentAtMs\":" + sentAtMs
                    + "}";
            WebPushSender.sendBase64Payload(
                    session.getPushEndpoint(),
                    session.getPushP256dhKey(),
                    session.getPushAuthKey(),
                    pushPayload
            );
        }
    }

    private boolean isBlank(String s) {
        return s == null || s.isBlank();
    }

    private static String jsonEscape(String s) {
        if (s == null) return "";
        StringBuilder out = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '\\') out.append("\\\\");
            else if (c == '"') out.append("\\\"");
            else if (c == '\n') out.append("\\n");
            else if (c == '\r') out.append("\\r");
            else if (c == '\t') out.append("\\t");
            else out.append(c);
        }
        return out.toString();
    }
}
