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_CallInviteBroadcast_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_CallInviteBroadcast_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_CallInviteBroadcast_Handler implements JsonMessageHandler {
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final int TYPE_INVITE = 100;
    private static final long PUSH_CALL_TTL_MS = 10_000L;

    @Override
    public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
        Net_CallInviteBroadcast_Request req = (Net_CallInviteBroadcast_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 callId = req.getCallId() == null ? "" : req.getCallId().trim();
        int type = req.getType() == null ? TYPE_INVITE : req.getType();
        if (toRequest.isBlank() || callId.isBlank() || type != TYPE_INVITE) {
            return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin/callId/type=100 обязательны");
        }

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

        String from = ctx.getLogin();
        String to = targetUser.getLogin();
        long timeMs = System.currentTimeMillis();
        long expiresAtMs = timeMs + PUSH_CALL_TTL_MS;

        Set<ConnectionContext> activeSessions = ActiveConnectionsRegistry.getInstance().getByLogin(to);
        List<ActiveSessionEntry> allTargetSessions = ActiveSessionsDAO.getInstance().getByLogin(to);

        int wsDelivered = 0;
        int webPushDelivered = 0;
        Set<String> activeSessionIds = new HashSet<>();

        for (ConnectionContext targetCtx : activeSessions) {
            activeSessionIds.add(targetCtx.getSessionId());

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

            boolean sent = WsEventSender.sendEvent(targetCtx, "IncomingCallInvite", eventId, payload);
            if (sent) wsDelivered++;
        }

        for (ActiveSessionEntry session : allTargetSessions) {
            String sessionId = String.valueOf(session.getSessionId() == null ? "" : session.getSessionId()).trim();
            if (!sessionId.isBlank() && activeSessionIds.contains(sessionId)) {
                continue;
            }
            if (isBlank(session.getPushEndpoint()) || isBlank(session.getPushP256dhKey()) || isBlank(session.getPushAuthKey())) {
                continue;
            }
            String payload = "{\"kind\":\"incoming_call\""
                    + ",\"title\":\"SHiNE: входящий звонок\""
                    + ",\"text\":\"Вам звонит " + jsonEscape(from) + "\""
                    + ",\"fromLogin\":\"" + jsonEscape(from) + "\""
                    + ",\"fromSessionId\":\"" + jsonEscape(ctx.getSessionId()) + "\""
                    + ",\"toLogin\":\"" + jsonEscape(to) + "\""
                    + ",\"callId\":\"" + jsonEscape(callId) + "\""
                    + ",\"sentAtMs\":" + timeMs
                    + ",\"expiresAtMs\":" + expiresAtMs
                    + "}";
            boolean pushed = WebPushSender.sendBase64Payload(
                    session.getPushEndpoint(),
                    session.getPushP256dhKey(),
                    session.getPushAuthKey(),
                    payload
            );
            if (pushed) webPushDelivered++;
        }

        Net_CallInviteBroadcast_Response resp = new Net_CallInviteBroadcast_Response();
        resp.setOp(req.getOp());
        resp.setRequestId(req.getRequestId());
        resp.setStatus(WireCodes.Status.OK);
        resp.setCallId(callId);
        resp.setDeliveredWsSessions(wsDelivered);
        resp.setDeliveredFcmSessions(webPushDelivered);
        resp.setDeliveredWebPushSessions(webPushDelivered);
        return resp;
    }

    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();
    }
}
