package server.logic.ws_protocol.JSON;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * Реестр активных подключений (только авторизованные).
 */
public final class ActiveConnectionsRegistry {

    private static final Logger log = LoggerFactory.getLogger(ActiveConnectionsRegistry.class);

    private static final ActiveConnectionsRegistry INSTANCE = new ActiveConnectionsRegistry();

    public static ActiveConnectionsRegistry getInstance() {
        return INSTANCE;
    }

    private ActiveConnectionsRegistry() {
        // singleton
    }

    // sessionId (String) -> ConnectionContext
    private final ConcurrentHashMap<String, ConnectionContext> bySessionId = new ConcurrentHashMap<>();

    // login (String) -> множество ConnectionContext для этого пользователя
    private final ConcurrentHashMap<String, Set<ConnectionContext>> byLogin = new ConcurrentHashMap<>();

    /**
     * Зарегистрировать авторизованное подключение.
     * Ожидается, что в ctx уже выставлены login и sessionId.
     */
    public void register(ConnectionContext ctx) {
        if (ctx == null) return;

        String sessionId = ctx.getSessionId();
        String login = ctx.getLogin();

        if (sessionId == null || sessionId.isBlank() || login == null || login.isBlank()) {
            log.debug("register skipped: bad ctx fields (login='{}', sessionId='{}')", login, sessionId);
            return;
        }

        // ✅ Если кто-то перерегистрировал тот же sessionId — вычищаем старый ctx из byLogin
        ConnectionContext prev = bySessionId.put(sessionId, ctx);
        if (prev != null && prev != ctx) {
            String prevLogin = prev.getLogin();
            if (prevLogin != null && !prevLogin.isBlank()) {
                Set<ConnectionContext> prevSet = byLogin.get(prevLogin);
                if (prevSet != null) {
                    prevSet.remove(prev);
                    if (prevSet.isEmpty()) {
                        byLogin.remove(prevLogin);
                    }
                }
            }
            log.warn("sessionId reused: replaced previous ctx (sessionId={}, prevLogin={}, newLogin={})",
                    sessionId, prevLogin, login);
        }

        byLogin
                .computeIfAbsent(login, id -> new CopyOnWriteArraySet<>())
                .add(ctx);

        log.debug("registered ctx (login={}, sessionId={})", login, sessionId);
    }

    /**
     * Удалить подключение по контексту (например, при onClose).
     */
    public void remove(ConnectionContext ctx) {
        if (ctx == null) return;

        String sessionId = ctx.getSessionId();
        String login = ctx.getLogin();

        if (sessionId != null && !sessionId.isBlank()) {
            ConnectionContext removed = bySessionId.remove(sessionId);

            // Если в мапе лежал другой ctx под тем же sessionId — не трогаем его byLogin
            if (removed != null && removed != ctx) {
                log.debug("remove(ctx): sessionId mapped to another ctx, skip byLogin cleanup (sessionId={})", sessionId);
                return;
            }
        }

        if (login != null && !login.isBlank()) {
            Set<ConnectionContext> set = byLogin.get(login);
            if (set != null) {
                set.remove(ctx);
                if (set.isEmpty()) {
                    byLogin.remove(login);
                }
            }
        }

        log.debug("removed ctx (login={}, sessionId={})", login, sessionId);
    }

    /**
     * Удалить подключение по sessionId.
     */
    public void removeBySessionId(String sessionId) {
        if (sessionId == null || sessionId.isBlank()) return;

        ConnectionContext ctx = bySessionId.remove(sessionId);
        if (ctx == null) return;

        String login = ctx.getLogin();
        if (login != null && !login.isBlank()) {
            Set<ConnectionContext> set = byLogin.get(login);
            if (set != null) {
                set.remove(ctx);
                if (set.isEmpty()) {
                    byLogin.remove(login);
                }
            }
        }

        log.debug("removed by sessionId (login={}, sessionId={})", login, sessionId);
    }

    /**
     * Получить контекст по sessionId.
     */
    public ConnectionContext getBySessionId(String sessionId) {
        if (sessionId == null || sessionId.isBlank()) return null;
        return bySessionId.get(sessionId);
    }

    /**
     * Получить все активные подключения пользователя по login.
     */
    public Set<ConnectionContext> getByLogin(String login) {
        if (login == null || login.isBlank()) return Set.of();
        Set<ConnectionContext> set = byLogin.get(login);
        return (set == null) ? Set.of() : set; // CopyOnWriteArraySet можно отдавать как есть
    }
}
package server.logic.ws_protocol.JSON;

import org.eclipse.jetty.websocket.api.Session;
import shine.db.entities.SolanaUserEntry;
import shine.db.entities.ActiveSessionEntry;

/**
 * ConnectionContext — контекст состояния одного WebSocket-соединения.
 * Живёт ровно столько же, сколько живёт подключение.
 *
 * Важно (v2):
 * - Авторизация всегда 2 шага:
 *   A) Создание новой сессии через deviceKey:
 *      AuthChallenge(login) -> ctx.authNonce
 *      CreateAuthSession(...) -> ctx.AUTH_STATUS_USER + ctx.activeSession
 *
 *   B) Вход в существующую сессию через sessionKey:
 *      SessionChallenge(sessionId) -> ctx.sessionLoginNonce + ctx.sessionLoginSessionId + expiresAt
 *      SessionLogin(...) -> проверка подписи sessionKey по pubkey из БД -> ctx.AUTH_STATUS_USER
 */
public class ConnectionContext {

    // Статусы аутентификации
    public static final int AUTH_STATUS_NONE = 0;              // анонимный / не авторизован
    public static final int AUTH_STATUS_AUTH_IN_PROGRESS = 1;  // выполнен challenge (AuthChallenge или SessionChallenge)
    public static final int AUTH_STATUS_USER = 2;              // авторизованный пользователь

    // Полный пользователь из БД (solana_users)
    private SolanaUserEntry solanaUserEntry;

    // Активная сессия из БД (active_sessions)
    private ActiveSessionEntry activeSessionEntry;

    /**
     * Идентификатор сессии — base64-строка от 32 байт.
     * Заполняется после успешного входа (AUTH_STATUS_USER).
     */
    private String sessionId;

    /**
     * Одноразовый nonce, выданный на шаге 1 (AuthChallenge),
     * используется на шаге CreateAuthSession для проверки подписи deviceKey.
     */
    private String authNonce;

    /* ===================== SessionLogin challenge (v2) ===================== */

    /**
     * Одноразовый nonce, выданный на шаге SessionChallenge(sessionId),
     * используется на шаге SessionLogin для проверки подписи sessionKey.
     */
    private String sessionLoginNonce;

    /**
     * sessionId, для которого был выдан sessionLoginNonce.
     * Нужен, чтобы SessionLogin не мог "подставить" другой sessionId.
     */
    private String sessionLoginSessionId;

    /**
     * Время истечения sessionLoginNonce (мс с 1970-01-01).
     * Если текущее время > expiresAt, то nonce считается недействительным.
     */
    private long sessionLoginNonceExpiresAtMs;

    /* ====================================================================== */

    /**
     * Текущий статус аутентификации.
     * См. константы AUTH_STATUS_*
     */
    private int authenticationStatus = AUTH_STATUS_NONE;

    /**
     * WebSocket-сессия Jetty для данного подключения.
     * Нужна, чтобы через ConnectionContext можно было отправлять сообщения клиенту.
     */
    private Session wsSession;

    // --- WebSocket Session ---

    public Session getWsSession() {
        return wsSession;
    }

    public void setWsSession(Session wsSession) {
        this.wsSession = wsSession;
    }

    // --- SolanaUser / ActiveSession ---

    public SolanaUserEntry getSolanaUser() {
        return solanaUserEntry;
    }

    public void setSolanaUser(SolanaUserEntry solanaUserEntry) {
        this.solanaUserEntry = solanaUserEntry;
    }

    public ActiveSessionEntry getActiveSession() {
        return activeSessionEntry;
    }

    public void setActiveSession(ActiveSessionEntry activeSessionEntry) {
        this.activeSessionEntry = activeSessionEntry;
    }

    // --- Удобный геттер для логина ---

    public String getLogin() {
        return solanaUserEntry != null ? solanaUserEntry.getLogin() : null;
    }

    // --- sessionId ---

    public String getSessionId() {
        return sessionId;
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }

    // --- authNonce ---

    public String getAuthNonce() {
        return authNonce;
    }

    public void setAuthNonce(String authNonce) {
        this.authNonce = authNonce;
    }

    // --- sessionLoginNonce (v2) ---

    public String getSessionLoginNonce() {
        return sessionLoginNonce;
    }

    public void setSessionLoginNonce(String sessionLoginNonce) {
        this.sessionLoginNonce = sessionLoginNonce;
    }

    public String getSessionLoginSessionId() {
        return sessionLoginSessionId;
    }

    public void setSessionLoginSessionId(String sessionLoginSessionId) {
        this.sessionLoginSessionId = sessionLoginSessionId;
    }

    public long getSessionLoginNonceExpiresAtMs() {
        return sessionLoginNonceExpiresAtMs;
    }

    public void setSessionLoginNonceExpiresAtMs(long sessionLoginNonceExpiresAtMs) {
        this.sessionLoginNonceExpiresAtMs = sessionLoginNonceExpiresAtMs;
    }

    // --- auth status ---

    public int getAuthenticationStatus() {
        return authenticationStatus;
    }

    public void setAuthenticationStatus(int authenticationStatus) {
        this.authenticationStatus = authenticationStatus;
    }

    public boolean isAuthenticatedUser() {
        return authenticationStatus == AUTH_STATUS_USER;
    }

    public boolean isAnonymous() {
        return authenticationStatus == AUTH_STATUS_NONE;
    }

    public void reset() {
        solanaUserEntry = null;
        activeSessionEntry = null;

        sessionId = null;
        authNonce = null;

        sessionLoginNonce = null;
        sessionLoginSessionId = null;
        sessionLoginNonceExpiresAtMs = 0;

        authenticationStatus = AUTH_STATUS_NONE;
        wsSession = null;
    }

    @Override
    public String toString() {
        return "ConnectionContext{" +
                "login='" + getLogin() + '\'' +
                ", sessionId=" + sessionId +
                ", authenticationStatus=" + authenticationStatus +
                '}';
    }
}
package server.logic.ws_protocol.JSON.entyties;

/**
 * Базовый класс для всех событий (event).
 * Общие поля: op и payload.
 *.
 * Формат JSON (event):
 * {
 *   "op": "...",
 *   "payload": { ... }
 * }
 */
public abstract class Net_Event {

    /** Имя операции / события (op). */
    private String op;

    /**
     * Произвольные данные.
     * В JSON это поле "payload".
     */
    private Object payload;

    // --- getters / setters ---

    public String getOp() {
        return op;
    }

    public void setOp(String op) {
        this.op = op;
    }

    public Object getPayload() {
        return payload;
    }

    public void setPayload(Object payload) {
        this.payload = payload;
    }
}

package server.logic.ws_protocol.JSON.entyties;

/**
 * Ответ с ошибкой (любой отказ).
 *.
 * В payload будет:
 * {
 *   "code": "...",
 *   "message": "..."
 * }
 */
public class Net_Exception_Response extends Net_Response {

    private String code;
    private String message;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

package server.logic.ws_protocol.JSON.entyties;

/**
 * Базовый класс для всех запросов (client → server).
 *.
 * Наследуется от NetEvent и добавляет requestId.
 *.
 * Формат JSON (request):
 * {
 *   "op": "...",
 *   "requestId": "...",
 *   "payload": { ... }
 * }
 */
public abstract class Net_Request extends Net_Event {

    /** Идентификатор запроса, чтобы связать запрос и ответ. */
    private String requestId;

    // --- getters / setters ---

    public String getRequestId() {
        return requestId;
    }

    public void setRequestId(String requestId) {
        this.requestId = requestId;
    }
}

package server.logic.ws_protocol.JSON.entyties;

/**
 * Базовый класс для всех ответов (server → client).
 *.
 * Наследуется от NetRequest и добавляет status.
 *.
 * Формат JSON (response):
 * {
 *   "op": "...",
 *   "requestId": "...",
 *   "status": 200,
 *   "payload": { ... } // и для успеха, и для ошибки
 * }
 */
public abstract class Net_Response extends Net_Request {

    /** Статус результата (200 — успех, любое другое значение — ошибка). */
    private int status;

    // --- getters / setters ---

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public boolean isOk() {
        return status == 200;
    }
}

package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Шаг 1 авторизации: запрос выдачи одноразового nonce (authNonce).
 *
 * Клиент по логину просит сервер сгенерировать случайный authNonce,
 * который будет использован на втором шаге при подписи.
 *
 * Формат входящего JSON:
 * {
 *   "op": "AuthChallenge",
 *   "requestId": "...",
 *   "payload": {
 *     "login": "someLogin"
 *   }
 * }
 *
 * Формат успешного ответа:
 * {
 *   "op": "AuthChallenge",
 *   "requestId": "...",
 *   "status": 200,
 *   "payload": {
 *     "authNonce": "base64-строка-от-32-байт"
 *   }
 * }
 */
public class Net_AuthChallenge_Request extends Net_Request {

    /**
     * Логин пользователя, для которого запускается авторизация.
     */
    private String login;

    public String getLogin() {
        return login;
    }
    public void setLogin(String login) {
        this.login = login;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Ответ на AuthChallenge.
 *
 * При успехе сервер возвращает одноразовый nonce для подписи (authNonce),
 * который клиент обязан использовать на втором шаге при формировании строки
 * для цифровой подписи.
 *
 * JSON:
 * {
 *   "op": "AuthChallenge",
 *   "requestId": "...",
 *   "status": 200,
 *   "payload": {
 *     "authNonce": "base64-строка-от-32-байт"
 *   }
 * }
 */
public class Net_AuthChallenge_Response extends Net_Response {

    /**
     * Одноразовый nonce для авторификации.
     * Строка — это base64-представление 32 случайных байт.
     */
    private String authNonce;

    public String getAuthNonce() {
        return authNonce;
    }

    public void setAuthNonce(String authNonce) {
        this.authNonce = authNonce;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Запрос CloseActiveSession — закрытие активной сессии пользователя.
 *
 * Новая логика (v2):
 * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
 * - Никаких подписей и "AUTH_IN_PROGRESS" здесь больше нет.
 *
 * payload:
 * {
 *   "sessionId": "..." // опционально; если пусто — закрываем текущую
 * }
 */
public class Net_CloseActiveSession_Request extends Net_Request {

    /** Идентификатор сессии, которую нужно закрыть. Может быть пустым. */
    private String sessionId;

    public String getSessionId() {
        return sessionId;
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Ответ на CloseActiveSession.
 *
 * При успехе:
 *  - status = 200;
 *  - payload = {}.
 *
 * Закрытие WebSocket-соединения может быть выполнено сразу (для другой сессии)
 * или чуть позже (для текущей сессии) после отправки ответа.
 */
public class Net_CloseActiveSession_Response extends Net_Response {
    // Дополнительных полей пока не требуется.
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Шаг 2 (v2): создание новой сессии ТОЛЬКО через deviceKey.
 *
 * Шаги:
 *  1) AuthChallenge(login) -> authNonce
 *  2) CreateAuthSession(storagePwd, sessionPubKeyB64, timeMs, signatureB64, clientInfo)
 *
 * Подпись deviceKey делается над строкой (UTF-8):
 *   AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}:{sessionPubKeyB64}:{storagePwd}
 *
 * Важно:
 * - sessionKey генерируется на клиенте, на сервер отправляется ТОЛЬКО sessionPubKeyB64 (32 bytes base64).
 * - В БД active_sessions.session_key хранится sessionPubKeyB64.
 */
public class Net_CreateAuthSession_Request extends Net_Request {

    /** Клиентский пароль для хранения данных (base64 от 32 байт). */
    private String storagePwd;

    /** Публичный ключ сессии (sessionPubKey), base64 от 32 байт. */
    private String sessionPubKeyB64;

    /** Время на стороне клиента (мс с 1970-01-01). */
    private long timeMs;

    /** Подпись Ed25519(deviceKey) над строкой AUTH_CREATE_SESSION:... (base64). */
    private String signatureB64;

    /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
    private String clientInfo;

    public String getStoragePwd() {
        return storagePwd;
    }

    public void setStoragePwd(String storagePwd) {
        this.storagePwd = storagePwd;
    }

    public String getSessionPubKeyB64() {
        return sessionPubKeyB64;
    }

    public void setSessionPubKeyB64(String sessionPubKeyB64) {
        this.sessionPubKeyB64 = sessionPubKeyB64;
    }

    public long getTimeMs() {
        return timeMs;
    }

    public void setTimeMs(long timeMs) {
        this.timeMs = timeMs;
    }

    public String getSignatureB64() {
        return signatureB64;
    }

    public void setSignatureB64(String signatureB64) {
        this.signatureB64 = signatureB64;
    }

    public String getClientInfo() {
        return clientInfo;
    }

    public void setClientInfo(String clientInfo) {
        this.clientInfo = clientInfo;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Ответ на CreateAuthSession (v2).
 *
 * При успехе сервер создаёт запись в active_sessions
 * и возвращает идентификатор сессии sessionId.
 *
 * JSON:
 * {
 *   "op": "CreateAuthSession",
 *   "requestId": "...",
 *   "status": 200,
 *   "payload": {
 *     "sessionId": "base64(32)"
 *   }
 * }
 */
public class Net_CreateAuthSession_Response extends Net_Response {

    /** Идентификатор сессии, base64 от 32 байт. */
    private String sessionId;

    public String getSessionId() {
        return sessionId;
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Запрос ListSessions — список активных сессий пользователя.
 *
 * Новая логика (v2):
 * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
 * - Пустой payload.
 */
public class Net_ListSessions_Request extends Net_Request {
    // пусто
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

import java.util.List;

/**
 * Ответ на ListSessions.
 *
 * При успехе:
 *  - status = 200;
 *  - payload:
 *    {
 *      "sessions": [
 *        {
 *          "sessionId": "...",
 *          "clientInfoFromClient": "...",
 *          "clientInfoFromRequest": "...",
 *          "geo": "Country, City" | "unknown",
 *          "lastAuthirificatedAtMs": 1733310000000
 *        },
 *        ...
 *      ]
 *    }
 */
public class Net_ListSessions_Response extends Net_Response {

    /**
     * Список активных сессий для текущего пользователя.
     */
    private List<SessionInfo> sessions;

    public List<SessionInfo> getSessions() {
        return sessions;
    }

    public void setSessions(List<SessionInfo> sessions) {
        this.sessions = sessions;
    }

    /**
     * Описание одной активной сессии.
     */
    public static class SessionInfo {

        /** Идентификатор сессии, base64 от 32 байт. */
        private String sessionId;

        /** Что прислал клиент в CreateAuthSession/RefreshSession (clientInfo). */
        private String clientInfoFromClient;

        /** Краткая строка, собранная сервером из HTTP-запроса (UA, платформа и т.п.). */
        private String clientInfoFromRequest;

        /** Строка геолокации вида "Country, City" или "unknown". */
        private String geo;

        /** Время последней успешной авторизации/refresh (мс с 1970-01-01). */
        private long lastAuthirificatedAtMs;

        // --- getters / setters ---

        public String getSessionId() {
            return sessionId;
        }

        public void setSessionId(String sessionId) {
            this.sessionId = sessionId;
        }

        public String getClientInfoFromClient() {
            return clientInfoFromClient;
        }

        public void setClientInfoFromClient(String clientInfoFromClient) {
            this.clientInfoFromClient = clientInfoFromClient;
        }

        public String getClientInfoFromRequest() {
            return clientInfoFromRequest;
        }

        public void setClientInfoFromRequest(String clientInfoFromRequest) {
            this.clientInfoFromRequest = clientInfoFromRequest;
        }

        public String getGeo() {
            return geo;
        }

        public void setGeo(String geo) {
            this.geo = geo;
        }

        public long getLastAuthirificatedAtMs() {
            return lastAuthirificatedAtMs;
        }

        public void setLastAuthirificatedAtMs(long lastAuthirificatedAtMs) {
            this.lastAuthirificatedAtMs = lastAuthirificatedAtMs;
        }
    }
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Шаг 1 входа в существующую сессию (v2):
 * SessionChallenge(sessionId) -> nonce
 */
public class Net_SessionChallenge_Request extends Net_Request {

    private String sessionId;

    public String getSessionId() {
        return sessionId;
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Ответ на SessionChallenge (v2).
 * payload: { "nonce": "base64(32)" }
 */
public class Net_SessionChallenge_Response extends Net_Response {

    private String nonce;

    public String getNonce() {
        return nonce;
    }

    public void setNonce(String nonce) {
        this.nonce = nonce;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Шаг 2 входа в существующую сессию (v2):
 * SessionLogin(sessionId, timeMs, signatureB64) -> storagePwd, AUTH_STATUS_USER
 *
 * Подпись делается sessionKey (приватный ключ на устройстве) над строкой (UTF-8):
 *   SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
 *
 * nonce берётся из SessionChallenge и хранится в ctx (одноразовый, TTL).
 */
public class Net_SessionLogin_Request extends Net_Request {

    private String sessionId;
    private long timeMs;
    private String signatureB64;

    /** Краткая строка от клиента (до 50 символов) с описанием устройства/клиента. */
    private String clientInfo;

    public String getSessionId() {
        return sessionId;
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }

    public long getTimeMs() {
        return timeMs;
    }

    public void setTimeMs(long timeMs) {
        this.timeMs = timeMs;
    }

    public String getSignatureB64() {
        return signatureB64;
    }

    public void setSignatureB64(String signatureB64) {
        this.signatureB64 = signatureB64;
    }

    public String getClientInfo() {
        return clientInfo;
    }

    public void setClientInfo(String clientInfo) {
        this.clientInfo = clientInfo;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Ответ на SessionLogin (v2).
 * payload: { "storagePwd": "base64(32)" }
 */
public class Net_SessionLogin_Response extends Net_Response {

    private String storagePwd;

    public String getStoragePwd() {
        return storagePwd;
    }

    public void setStoragePwd(String storagePwd) {
        this.storagePwd = storagePwd;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth;

import server.logic.ws_protocol.Base64Ws;
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.handlers.auth.entyties.Net_AuthChallenge_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.SolanaUserEntry;

import java.security.SecureRandom;

/**
 * AuthChallenge (v2) — шаг 1 создания новой сессии.
 *
 * Логика авторизации (v2):
 * - Создание новой сессии возможно ТОЛЬКО через deviceKey пользователя.
 * - Этот handler выдаёт одноразовый authNonce, который клиент использует во втором шаге:
 *   CreateAuthSession(..., signature(deviceKey, AUTH_CREATE_SESSION:...))
 *
 * Что делает:
 * 1) Проверяет login.
 * 2) Находит пользователя (solana_users).
 * 3) Пишет solanaUser в ctx, ставит AUTH_STATUS_AUTH_IN_PROGRESS.
 * 4) Генерирует authNonce (base64url(32)) и сохраняет в ctx.authNonce.
 */
public class Net_AuthChallenge_Handler implements JsonMessageHandler {

    private static final SecureRandom RANDOM = new SecureRandom();

    @Override
    public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {

        Net_AuthChallenge_Request req = (Net_AuthChallenge_Request) baseReq;

        String login = req.getLogin();
        if (login == null || login.isBlank()) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "EMPTY_LOGIN",
                    "Пустой логин"
            );
        }

        // Если по этому соединению уже есть залогиненный пользователь — не даём повторную авторификацию
        if (ctx.getLogin() != null) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "ALREADY_AUTHED",
                    "Попытка повторной авторификации для уже заданного login=" + ctx.getLogin()
            );
        }

        SolanaUserEntry solanaUserEntry = SolanaUsersDAO.getInstance().getByLogin(login);
        if (solanaUserEntry == null) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.UNVERIFIED,
                    "UNKNOWN_USER",
                    "Пользователь с таким логином не найден"
            );
        }

        ctx.setSolanaUser(solanaUserEntry);
        ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS);

        byte[] buf = new byte[32];
        RANDOM.nextBytes(buf);
        String authNonce = Base64Ws.encode(buf);

        ctx.setAuthNonce(authNonce);

        Net_AuthChallenge_Response resp = new Net_AuthChallenge_Response();
        resp.setOp(req.getOp());
        resp.setRequestId(req.getRequestId());
        resp.setStatus(WireCodes.Status.OK);
        resp.setAuthNonce(authNonce);

        return resp;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.handlers.auth.entyties.Net_CloseActiveSession_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import server.ws.WsConnectionUtils;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;

import java.sql.SQLException;

/**
 * CloseActiveSession (v2) — закрытие текущей или другой сессии.
 *
 * Логика авторизации (v2):
 * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
 * - Никаких подписей и AUTH_IN_PROGRESS здесь больше нет.
 *
 * Закрытие:
 * - удаляем запись из БД
 * - если по sessionId есть активный WS — закрываем его
 */
public class Net_CloseActiveSession_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_CloseActiveSession_Handler.class);

    @Override
    public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
        Net_CloseActiveSession_Request req = (Net_CloseActiveSession_Request) baseReq;

        if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.UNVERIFIED,
                    "NOT_AUTHENTICATED",
                    "Операция доступна только для авторизованных пользователей"
            );
        }

        SolanaUserEntry user = ctx.getSolanaUser();
        String currentLogin = user.getLogin();

        String targetSessionId = req.getSessionId();
        if (targetSessionId == null || targetSessionId.isBlank()) {
            if (ctx.getSessionId() != null && !ctx.getSessionId().isBlank()) {
                targetSessionId = ctx.getSessionId();
            } else if (ctx.getActiveSession() != null && ctx.getActiveSession().getSessionId() != null) {
                targetSessionId = ctx.getActiveSession().getSessionId();
            } else {
                return NetExceptionResponseFactory.error(
                        req,
                        WireCodes.Status.BAD_REQUEST,
                        "NO_SESSION_TO_CLOSE",
                        "Не удалось определить, какую сессию нужно закрыть"
                );
            }
        }

        ActiveSessionEntry targetSession;
        try {
            targetSession = ActiveSessionsDAO.getInstance().getBySessionId(targetSessionId);
        } catch (SQLException e) {
            log.error("Ошибка БД при поиске сессии для CloseActiveSession sessionId={}", targetSessionId, e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "DB_ERROR",
                    "Ошибка доступа к базе данных при поиске сессии"
            );
        }

        if (targetSession == null) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.UNVERIFIED,
                    "SESSION_NOT_FOUND",
                    "Сессия для закрытия не найдена"
            );
        }

        if (currentLogin == null || !currentLogin.equals(targetSession.getLogin())) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.UNVERIFIED,
                    "SESSION_OF_ANOTHER_USER",
                    "Нельзя закрывать сессию другого пользователя"
            );
        }

        boolean isCurrentSession = targetSessionId.equals(ctx.getSessionId());

        closeActiveSession(targetSessionId, ctx, isCurrentSession);

        Net_CloseActiveSession_Response resp = new Net_CloseActiveSession_Response();
        resp.setOp(req.getOp());
        resp.setRequestId(req.getRequestId());
        resp.setStatus(WireCodes.Status.OK);
        return resp;
    }

    private void closeActiveSession(String targetSessionId,
                                    ConnectionContext currentCtx,
                                    boolean isCurrentSession) {

        try {
            ActiveSessionsDAO.getInstance().deleteBySessionId(targetSessionId);
        } catch (SQLException e) {
            log.error("Ошибка БД при удалении сессии sessionId={}", targetSessionId, e);
        }

        ConnectionContext ctxToClose =
                ActiveConnectionsRegistry.getInstance().getBySessionId(targetSessionId);

        if (ctxToClose == null) return;

        if (isCurrentSession && ctxToClose == currentCtx) {
            new Thread(() -> {
                try { Thread.sleep(50); } catch (InterruptedException ignored) {}
                WsConnectionUtils.closeConnection(
                        ctxToClose,
                        4000,
                        "Session closed by client via CloseActiveSession"
                );
            }, "CloseSession-" + targetSessionId).start();
        } else {
            WsConnectionUtils.closeConnection(
                    ctxToClose,
                    4000,
                    "Session closed by client via CloseActiveSession"
            );
        }
    }
}
package server.logic.ws_protocol.JSON.handlers.auth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.Base64Ws;
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.handlers.auth.entyties.Net_CreateAuthSession_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import server.ws.WsConnectionUtils;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import shine.geo.ClientInfoService;
import shine.geo.GeoLookupService;
import utils.crypto.Ed25519Util;

import org.eclipse.jetty.websocket.api.Session;

import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.sql.SQLException;

/**
 * CreateAuthSession (v2) — шаг 2 создания новой сессии (ТОЛЬКО deviceKey).
 *
 * Логика авторизации (v2):
 *  - Создание сессии: AuthChallenge(login) -> authNonce -> CreateAuthSession(...)
 *  - Клиент генерирует sessionKey (Ed25519), хранит приватный ключ у себя,
 *    отправляет на сервер ТОЛЬКО sessionPubKeyB64.
 *  - Сервер сохраняет sessionPubKeyB64 в active_sessions.session_key.
 *
 * Подпись deviceKey (Ed25519) проверяется над строкой (UTF-8):
 *   AUTH_CREATE_SESSION:{login}:{timeMs}:{authNonce}
 *
 * На выходе:
 *  - создаётся запись active_sessions
 *  - ctx становится AUTH_STATUS_USER (вход выполнен как "текущая сессия")
 *  - ответ: sessionId
 */
public class Net_CreateAuthSession__Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class);
    private static final SecureRandom RANDOM = new SecureRandom();

    public static final long ALLOWED_SKEW_MS = 30_000L;

    @Override
    public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {

        Net_CreateAuthSession_Request req = (Net_CreateAuthSession_Request) baseReq;

        if (ctx == null
                || ctx.getSolanaUser() == null
                || ctx.getAuthNonce() == null
                || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_AUTH_IN_PROGRESS) {

            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "NO_STEP1_CONTEXT",
                    "Шаг 1 авторизации не был корректно выполнен для данного соединения"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no step1 context or bad auth state");
            return err;
        }

        SolanaUserEntry user = ctx.getSolanaUser();
        String login = user.getLogin();
        if (login == null || login.isBlank()) {
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "NO_LOGIN",
                    "Для пользователя не задан login в БД"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no login");
            return err;
        }

        String storagePwd = req.getStoragePwd();
        if (storagePwd == null || storagePwd.isBlank()) {
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "EMPTY_STORAGE_PWD",
                    "Пустой storagePwd"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty storagePwd");
            return err;
        }

        String sessionPubKeyB64 = req.getSessionPubKeyB64();
        if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "EMPTY_SESSION_PUBKEY",
                    "Пустой sessionPubKeyB64"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty session pubkey");
            return err;
        }

        // Проверим, что sessionPubKeyB64 декодируется в 32 байта
        byte[] sessionPubKey32;
        try {
            sessionPubKey32 = Base64Ws.decode(sessionPubKeyB64);
        } catch (IllegalArgumentException e) {
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_BASE64",
                    "Некорректный base64 в sessionPubKeyB64"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey base64");
            return err;
        }
        if (sessionPubKey32.length != 32) {
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_SESSION_PUBKEY_LEN",
                    "sessionPubKey должен быть 32 байта"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad session pubkey length");
            return err;
        }

        String signatureB64 = req.getSignatureB64();
        if (signatureB64 == null || signatureB64.isBlank()) {
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "EMPTY_SIGNATURE",
                    "Пустая цифровая подпись"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: empty signature");
            return err;
        }

        long timeMs = req.getTimeMs();
        long nowMs = System.currentTimeMillis();
        long diff = Math.abs(nowMs - timeMs);
        if (diff > ALLOWED_SKEW_MS) {
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "TIME_SKEW",
                    "Время клиента отличается от сервера более чем на 30 секунд"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: time skew");
            return err;
        }

        String clientInfoFromClient = req.getClientInfo();
        if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
            clientInfoFromClient = clientInfoFromClient.substring(0, 50);
        }

        String devicePubKeyB64 = user.getDeviceKey();
        if (devicePubKeyB64 == null || devicePubKeyB64.isBlank()) {
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "NO_DEVICE_KEY",
                    "Отсутствует deviceKey у пользователя"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: no deviceKey");
            return err;
        }

        String authNonce = ctx.getAuthNonce();

        boolean sigOk;
        try {
            sigOk = verifyCreateSessionSignature(
                    user,
                    login,
                    authNonce,
                    timeMs,
                    signatureB64
            );
        } catch (IllegalArgumentException ex) {
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_BASE64",
                    "Некорректный формат Base64 для ключа или подписи"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad base64");
            return err;
        }

        if (!sigOk) {
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.UNVERIFIED,
                    "BAD_SIGNATURE",
                    "Подпись не прошла проверку"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: bad signature");
            return err;
        }

        // --- генерируем sessionId ---
        String sessionId = generateRandom32B64Url();
        long now = System.currentTimeMillis();

        // --- Сбор данных о клиенте (IP, UA, язык) ---
        Session wsSession = ctx.getWsSession();
        String clientInfoFromRequest = ClientInfoService.buildClientInfoString(wsSession);
        String userLanguage = ClientInfoService.extractPreferredLanguageTag(wsSession);

        String clientIp = "";
        if (wsSession != null) {
            String ip = ClientInfoService.extractClientIp(wsSession);
            if (ip != null) clientIp = ip;

            if (!clientIp.isBlank()) {
                try {
                    GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
                } catch (Exception e) {
                    log.debug("Geo lookup failed for ip={}", clientIp, e);
                }
            }
        }

        // --- создаём запись ActiveSession и сохраняем в БД ---
        ActiveSessionsDAO dao = ActiveSessionsDAO.getInstance();
        ActiveSessionEntry activeSessionEntry;

        try {
            activeSessionEntry = new ActiveSessionEntry(
                    sessionId,
                    login,
                    sessionPubKeyB64,         // session_key (pubkey)
                    storagePwd,
                    now,
                    now,
                    null,                    // pushEndpoint
                    null,                    // pushP256dhKey
                    null,                    // pushAuthKey
                    clientIp,
                    clientInfoFromClient,
                    clientInfoFromRequest,
                    userLanguage
            );

            dao.insert(activeSessionEntry);
        } catch (SQLException e) {
            log.error("Ошибка БД при создании новой сессии для login={}", login, e);
            Net_Response err = NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "DB_ERROR_SESSION_CREATE",
                    "Ошибка БД при создании сессии"
            );
            WsConnectionUtils.closeConnection(ctx, 4001, "Auth failed: db error");
            return err;
        }

        // --- обновляем контекст ---
        ctx.setActiveSession(activeSessionEntry);
        ctx.setSessionId(sessionId);
        ctx.setAuthNonce(null);
        ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);

        ActiveConnectionsRegistry.getInstance().register(ctx);

        // --- формируем ответ ---
        Net_CreateAuthSession_Response resp = new Net_CreateAuthSession_Response();
        resp.setOp(req.getOp());
        resp.setRequestId(req.getRequestId());
        resp.setStatus(WireCodes.Status.OK);
        resp.setSessionId(sessionId);
        return resp;
    }

    private static boolean verifyCreateSessionSignature(
            SolanaUserEntry user,
            String login,
            String authNonce,
            long timeMs,
            String signatureB64
    ) throws IllegalArgumentException {

        // deviceKey (pub, 32)
        byte[] publicKey32 = Ed25519Util.keyFromBase64(user.getDeviceKey());
        byte[] signature64 = Base64Ws.decode(signatureB64);

        String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
        byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);

        return Ed25519Util.verify(preimage, signature64, publicKey32);
    }

    private static String generateRandom32B64Url() {
        byte[] buf = new byte[32];
        RANDOM.nextBytes(buf);
        return Base64Ws.encode(buf);
    }
}
package server.logic.ws_protocol.JSON.handlers.auth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.handlers.auth.entyties.Net_ListSessions_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Response.SessionInfo;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.SolanaUserEntry;
import shine.geo.GeoLookupService;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * ListSessions (v2) — список активных сессий.
 *
 * Логика авторизации (v2):
 * - Доступно ТОЛЬКО после успешного входа в сессию (AUTH_STATUS_USER).
 * - Никаких подписей здесь больше нет.
 */
public class Net_ListSessions_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_ListSessions_Handler.class);

    @Override
    public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
        Net_ListSessions_Request req = (Net_ListSessions_Request) baseReq;

        if (ctx == null || ctx.getSolanaUser() == null || ctx.getAuthenticationStatus() != ConnectionContext.AUTH_STATUS_USER) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.UNVERIFIED,
                    "NOT_AUTHENTICATED",
                    "Операция доступна только для авторизованных пользователей"
            );
        }

        SolanaUserEntry user = ctx.getSolanaUser();
        String currentLogin = user.getLogin();

        List<ActiveSessionEntry> sessions;
        try {
            sessions = ActiveSessionsDAO.getInstance().getByLogin(currentLogin);
        } catch (SQLException e) {
            log.error("Ошибка БД при получении списка сессий для login={}", currentLogin, e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "DB_ERROR_LIST_SESSIONS",
                    "Ошибка доступа к базе данных при получении списка сессий"
            );
        }

        List<SessionInfo> resultList = new ArrayList<>();
        for (ActiveSessionEntry s : sessions) {
            SessionInfo info = new SessionInfo();
            info.setSessionId(s.getSessionId());
            info.setClientInfoFromClient(s.getClientInfoFromClient());
            info.setClientInfoFromRequest(s.getClientInfoFromRequest());
            info.setLastAuthirificatedAtMs(s.getLastAuthirificatedAtMs());

            String ip = s.getClientIp();
            String geo = GeoLookupService.resolveCountryCityOrIpWithCache(ip);
            info.setGeo(geo);

            resultList.add(info);
        }

        Net_ListSessions_Response resp = new Net_ListSessions_Response();
        resp.setOp(req.getOp());
        resp.setRequestId(req.getRequestId());
        resp.setStatus(WireCodes.Status.OK);
        resp.setSessions(resultList);

        return resp;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth;

import server.logic.ws_protocol.Base64Ws;
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.handlers.auth.entyties.Net_SessionChallenge_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.entities.ActiveSessionEntry;

import java.security.SecureRandom;
import java.sql.SQLException;

/**
 * SessionChallenge (v2) — шаг 1 входа в существующую сессию.
 *
 * Логика авторизации (v2):
 * - Вход в существующую сессию ВСЕГДА в 2 шага:
 *   1) SessionChallenge(sessionId) -> nonce
 *   2) SessionLogin(sessionId, timeMs, signature(sessionKey, SESSION_LOGIN:...))
 *
 * Что делает:
 * - Проверяет, что sessionId существует в БД.
 * - Генерирует одноразовый nonce (base64url(32)), сохраняет его в ctx:
 *   ctx.sessionLoginNonce, ctx.sessionLoginSessionId, ctx.sessionLoginNonceExpiresAtMs.
 */
public class Net_SessionChallenge_Handler implements JsonMessageHandler {

    private static final SecureRandom RANDOM = new SecureRandom();
    private static final long NONCE_TTL_MS = 60_000L;

    @Override
    public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
        Net_SessionChallenge_Request req = (Net_SessionChallenge_Request) baseReq;

        String sessionId = req.getSessionId();
        if (sessionId == null || sessionId.isBlank()) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "EMPTY_SESSION_ID",
                    "Пустой sessionId"
            );
        }

        ActiveSessionEntry session;
        try {
            session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
        } catch (SQLException e) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "DB_ERROR",
                    "Ошибка доступа к базе данных"
            );
        }

        if (session == null) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.UNVERIFIED,
                    "SESSION_NOT_FOUND",
                    "Сессия не найдена"
            );
        }

        byte[] buf = new byte[32];
        RANDOM.nextBytes(buf);
        String nonce = Base64Ws.encode(buf);

        long now = System.currentTimeMillis();
        ctx.setSessionLoginNonce(nonce);
        ctx.setSessionLoginSessionId(sessionId);
        ctx.setSessionLoginNonceExpiresAtMs(now + NONCE_TTL_MS);

        Net_SessionChallenge_Response resp = new Net_SessionChallenge_Response();
        resp.setOp(req.getOp());
        resp.setRequestId(req.getRequestId());
        resp.setStatus(WireCodes.Status.OK);
        resp.setNonce(nonce);
        return resp;
    }
}
package server.logic.ws_protocol.JSON.handlers.auth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.Base64Ws;
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.handlers.auth.entyties.Net_SessionLogin_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
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 shine.geo.ClientInfoService;
import shine.geo.GeoLookupService;
import utils.crypto.Ed25519Util;

import java.nio.charset.StandardCharsets;
import java.sql.SQLException;

/**
 * SessionLogin (v2) — шаг 2 входа в существующую сессию (по sessionKey).
 *
 * Логика авторизации (v2):
 * - SessionChallenge(sessionId) выдаёт nonce (одноразовый, TTL).
 * - SessionLogin проверяет подпись sessionKey над строкой:
 *     SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
 * - sessionPubKey берём из БД: active_sessions.session_key (base64 32 bytes).
 *
 * При успехе:
 * - ctx становится AUTH_STATUS_USER
 * - обновляем метаданные сессии (lastAuth + clientIp + clientInfo + lang)
 * - возвращаем storagePwd
 */
public class Net_SessionLogin_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class);

    private static final long ALLOWED_SKEW_MS = 30_000L;

    @Override
    public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
        Net_SessionLogin_Request req = (Net_SessionLogin_Request) baseReq;

        String sessionId = req.getSessionId();
        if (sessionId == null || sessionId.isBlank()) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "EMPTY_SESSION_ID",
                    "Пустой sessionId"
            );
        }

        // проверка челленджа
        if (ctx.getSessionLoginNonce() == null
                || ctx.getSessionLoginSessionId() == null
                || System.currentTimeMillis() > ctx.getSessionLoginNonceExpiresAtMs()) {

            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "NO_CHALLENGE",
                    "Нет активного SessionChallenge или nonce истёк"
            );
        }

        if (!sessionId.equals(ctx.getSessionLoginSessionId())) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "SESSION_ID_MISMATCH",
                    "nonce был выдан для другого sessionId"
            );
        }

        long timeMs = req.getTimeMs();
        long nowMs = System.currentTimeMillis();
        if (Math.abs(nowMs - timeMs) > ALLOWED_SKEW_MS) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "TIME_SKEW",
                    "Время клиента отличается от сервера более чем на 30 секунд"
            );
        }

        String signatureB64 = req.getSignatureB64();
        if (signatureB64 == null || signatureB64.isBlank()) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "EMPTY_SIGNATURE",
                    "Пустая подпись"
            );
        }

        ActiveSessionEntry session;
        try {
            session = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
        } catch (SQLException e) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "DB_ERROR",
                    "Ошибка доступа к базе данных"
            );
        }

        if (session == null) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.UNVERIFIED,
                    "SESSION_NOT_FOUND",
                    "Сессия не найдена"
            );
        }

        String sessionPubKeyB64 = session.getSessionKey(); // это pubKey (Base64(32))
        if (sessionPubKeyB64 == null || sessionPubKeyB64.isBlank()) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "NO_SESSION_KEY",
                    "В сессии не задан session_key"
            );
        }

        String nonce = ctx.getSessionLoginNonce();

        boolean sigOk;
        try {
            sigOk = verifySessionLoginSignature(sessionPubKeyB64, sessionId, timeMs, nonce, signatureB64);
        } catch (IllegalArgumentException e) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_BASE64",
                    "Некорректный Base64 для ключа/подписи"
            );
        }

        if (!sigOk) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.UNVERIFIED,
                    "BAD_SIGNATURE",
                    "Подпись не прошла проверку"
            );
        }

        // сжигаем nonce
        ctx.setSessionLoginNonce(null);
        ctx.setSessionLoginSessionId(null);
        ctx.setSessionLoginNonceExpiresAtMs(0);

        // подтягиваем пользователя
        SolanaUserEntry user;
        try {
            user = SolanaUsersDAO.getInstance().getByLogin(session.getLogin());
        } catch (SQLException e) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "DB_ERROR_USER_LOOKUP",
                    "Ошибка доступа к базе данных при получении пользователя"
            );
        }

        if (user == null) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.UNVERIFIED,
                    "USER_NOT_FOUND_FOR_SESSION",
                    "Пользователь для данной сессии не найден"
            );
        }

        // обновление метаданных
        String clientInfoFromClient = req.getClientInfo();
        if (clientInfoFromClient != null && clientInfoFromClient.length() > 50) {
            clientInfoFromClient = clientInfoFromClient.substring(0, 50);
        }

        String clientIp = null;
        String clientInfoFromRequest = null;
        String userLanguage = null;

        if (ctx.getWsSession() != null) {
            clientIp = ClientInfoService.extractClientIp(ctx.getWsSession());
            clientInfoFromRequest = ClientInfoService.buildClientInfoString(ctx.getWsSession());
            userLanguage = ClientInfoService.extractPreferredLanguageTag(ctx.getWsSession());

            if (clientIp != null && !clientIp.isBlank()) {
                try {
                    GeoLookupService.resolveCountryCityOrIpWithCache(clientIp);
                } catch (Exception e) {
                    log.debug("Geo lookup failed for ip={}", clientIp, e);
                }
            }
        }

        long now = System.currentTimeMillis();
        try {
            ActiveSessionsDAO.getInstance().updateOnRefresh(
                    sessionId,
                    now,
                    clientIp,
                    clientInfoFromClient,
                    clientInfoFromRequest,
                    userLanguage
            );
        } catch (SQLException e) {
            log.error("Ошибка БД при updateOnRefresh sessionId={}", sessionId, e);
        }

        session.setLastAuthirificatedAtMs(now);
        session.setClientIp(clientIp);
        session.setClientInfoFromClient(clientInfoFromClient);
        session.setClientInfoFromRequest(clientInfoFromRequest);
        session.setUserLanguage(userLanguage);

        // ctx
        ctx.setActiveSession(session);
        ctx.setSolanaUser(user);
        ctx.setSessionId(sessionId);
        ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER);

        ActiveConnectionsRegistry.getInstance().register(ctx);

        // ответ
        Net_SessionLogin_Response resp = new Net_SessionLogin_Response();
        resp.setOp(req.getOp());
        resp.setRequestId(req.getRequestId());
        resp.setStatus(WireCodes.Status.OK);
        resp.setStoragePwd(session.getStoragePwd());
        return resp;
    }

    private static boolean verifySessionLoginSignature(
            String sessionPubKeyB64,
            String sessionId,
            long timeMs,
            String nonce,
            String signatureB64
    ) throws IllegalArgumentException {

        // pubKey: Base64(32). (Ed25519Util.keyFromBase64 должен использовать стандартный Base64)
        byte[] publicKey32 = Ed25519Util.keyFromBase64(sessionPubKeyB64);

        // signature: Base64(64) через единую утилиту WS-протокола
        byte[] signature64 = Base64Ws.decodeLen(signatureB64, 64, "signatureB64");

        String preimageStr = "SESSION_LOGIN:" + sessionId + ":" + timeMs + ":" + nonce;
        byte[] preimage = preimageStr.getBytes(StandardCharsets.UTF_8);

        return Ed25519Util.verify(preimage, signature64, publicKey32);
    }
}
package server.logic.ws_protocol.JSON.handlers.blockchain.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

public final class Net_AddBlock_Request extends Net_Request {

    private String blockchainName;   // обязателен
    private int blockNumber;         // обязателен
    private String prevBlockHash;    // HEX(64) или "" для нулевого
    private String blockBytesB64;    // байты FULL-блока (raw+sig+hash) в Base64

    public String getBlockchainName() { return blockchainName; }
    public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }

    public int getBlockNumber() { return blockNumber; }
    public void setBlockNumber(int blockNumber) { this.blockNumber = blockNumber; }

    public String getPrevBlockHash() { return prevBlockHash; }
    public void setPrevBlockHash(String prevBlockHash) { this.prevBlockHash = prevBlockHash; }

    public String getBlockBytesB64() { return blockBytesB64; }
    public void setBlockBytesB64(String blockBytesB64) { this.blockBytesB64 = blockBytesB64; }
}
package server.logic.ws_protocol.JSON.handlers.blockchain.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Ответ:
 * - reasonCode (null если ok)
 * - serverLastGlobalNumber / serverLastGlobalHash
 */
public final class Net_AddBlock_Response extends Net_Response {

    /** null если ok, иначе строка причины (bad_block_base64, user_not_found, и т.п.) */
    private String reasonCode;

    /** что сервер считает последним по глобальной цепочке */
    private int serverLastGlobalNumber;
    private String serverLastGlobalHash;

    public String getReasonCode() { return reasonCode; }
    public void setReasonCode(String reasonCode) { this.reasonCode = reasonCode; }

    public int getServerLastGlobalNumber() { return serverLastGlobalNumber; }
    public void setServerLastGlobalNumber(int v) { this.serverLastGlobalNumber = v; }

    public String getServerLastGlobalHash() { return serverLastGlobalHash; }
    public void setServerLastGlobalHash(String v) { this.serverLastGlobalHash = v; }
}
package server.logic.ws_protocol.JSON.handlers.blockchain;

import blockchain.BchBlockEntry;
import blockchain.BchCryptoVerifier;
import blockchain.MsgSubType;
import blockchain.body.BodyHasLine;
import blockchain.body.BodyHasTarget;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.Base64Ws;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.entyties.Net_Exception_Response;
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.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainLocks;
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.BlockEntry;
import utils.blockchain.BlockchainNameUtil;

import java.util.Arrays;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Net_AddBlock_Handler — единый хэндлер добавления блока (JSON).
 *
 * Изменение (v3):
 * - ВСЕ ошибки теперь возвращаются в стандартном формате Net_Exception_Response:
 *   status != 200, payload: { code, message, serverLastGlobalNumber, serverLastGlobalHash }
 * - Успех — как и раньше Net_AddBlock_Response (status=200).
 */
public final class Net_AddBlock_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_AddBlock_Handler.class);

    private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
    private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();

    private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO);

    @Override
    public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) {

        Net_AddBlock_Request req = (Net_AddBlock_Request) baseReq;

        String blockchainName = req.getBlockchainName();
        ReentrantLock lock = BlockchainLocks.lockFor(blockchainName);
        lock.lock();
        try {
            AddBlockResult r = addBlock(
                    blockchainName,
                    req.getBlockNumber(),        // старое поле, пока оставляем
                    req.getPrevBlockHash(),      // старое поле, пока оставляем
                    req.getBlockBytesB64()
            );

            // ✅ УСПЕХ: как раньше
            if (r.isOk()) {
                Net_AddBlock_Response resp = new Net_AddBlock_Response();
                resp.setOp(req.getOp());
                resp.setRequestId(req.getRequestId());
                resp.setStatus(WireCodes.Status.OK);

                resp.setReasonCode(null);
                resp.setServerLastGlobalNumber(r.serverLastBlockNumber);
                resp.setServerLastGlobalHash(r.serverLastBlockHashHex);

                return resp;
            }

            // ✅ ОШИБКА: стандартный формат (code + message) + доп.поля для ресинка
            return error(req, r.httpStatus, r.reasonCode, r.serverLastBlockNumber, r.serverLastBlockHashHex);

        } finally {
            lock.unlock();
        }
    }

    private Net_Response error(Net_AddBlock_Request req,
                               int status,
                               String reasonCode,
                               int serverLastNum,
                               String serverLastHashHex) {

        AddBlockExceptionResponse resp = new AddBlockExceptionResponse();
        resp.setOp(req.getOp());
        resp.setRequestId(req.getRequestId());
        resp.setStatus(status);

        // code — машинный
        resp.setCode(reasonCode != null ? reasonCode : "add_block_error");
        // message — человеческий (можешь улучшать тексты как угодно)
        resp.setMessage(humanMessage(reasonCode));

        // полезно клиенту для ресинка
        resp.setServerLastGlobalNumber(serverLastNum);
        resp.setServerLastGlobalHash(serverLastHashHex);

        return resp;
    }

    private static String humanMessage(String code) {
        if (code == null) return "Ошибка добавления блока";

        return switch (code) {
            case "empty_blockchain_name" -> "Пустое имя блокчейна";
            case "bad_blockchain_name" -> "Некорректное имя блокчейна";
            case "db_error" -> "Ошибка базы данных";
            case "blockchain_state_not_found" -> "Состояние блокчейна не найдено";
            case "state_last_hash_invalid" -> "Повреждено состояние блокчейна: неверный last_block_hash";
            case "bad_block_base64" -> "Некорректный base64 блока";
            case "limit_exceeded" -> "Превышен лимит размера блокчейна";
            case "limit_check_failed" -> "Ошибка проверки лимита размера";
            case "bad_block_format" -> "Некорректный формат блока";
            case "bad_block_body" -> "Некорректное тело блока";
            case "bad_block_number" -> "Некорректный номер блока";
            case "req_global_mismatch" -> "Номер блока в запросе не совпадает с номером в блоке";
            case "bad_prev_hash" -> "Некорректный prevHash (цепочка не совпадает)";
            case "bad_blockchain_key_len" -> "Некорректный ключ блокчейна в состоянии (ожидалось 32 байта)";
            case "signature_verify_failed" -> "Ошибка проверки подписи блока";
            case "bad_signature" -> "Некорректная подпись блока";
            case "prev_line_block_not_found" -> "Не найден блок prevLineNumber для проверки линии";
            case "bad_prev_line_hash" -> "Некорректный prevLineHash";
            case "db_error_prev_line_check" -> "Ошибка БД при проверке prevLine";
            case "internal_error" -> "Внутренняя ошибка сервера при записи блока";
            default -> "Ошибка: " + code;
        };
    }

    private AddBlockResult addBlock(
            String blockchainName,
            int globalNumberFromReq,
            String prevGlobalHashHexFromReq,
            String blockBytesB64
    ) {
        if (blockchainName == null || blockchainName.isBlank()) {
            log.warn("AddBlock: пустой blockchainName (reqGlobalNumber={})", globalNumberFromReq);
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "empty_blockchain_name", 0, "");
        }

        String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
        if (login == null || login.isBlank()) {
            log.warn("AddBlock: плохой blockchainName='{}' => login не получился (reqGlobalNumber={})",
                    blockchainName, globalNumberFromReq);
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_name", 0, "");
        }

        // 1) state обязателен
        final BlockchainStateEntry st;
        try {
            st = stateDAO.getByBlockchainName(blockchainName);
        } catch (Exception e) {
            log.error("AddBlock: ошибка БД при чтении blockchain_state (login={}, blockchainName={}, reqGlobalNumber={})",
                    login, blockchainName, globalNumberFromReq, e);
            return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error", 0, "");
        }

        if (st == null) {
            log.warn("AddBlock: blockchain_state_not_found (login={}, blockchainName={}, reqGlobalNumber={})",
                    login, blockchainName, globalNumberFromReq);
            return new AddBlockResult(WireCodes.Status.NOT_FOUND, "blockchain_state_not_found", -1, "");
        }

        final int serverLastNum = st.getLastBlockNumber();

        final byte[] serverLastHash32;
        try {
            serverLastHash32 = (serverLastNum < 0)
                    ? new byte[32]
                    : require32OrThrow(st.getLastBlockHash(), "state.last_block_hash is null/invalid");
        } catch (Exception e) {
            // ✅ Раньше тут мог вылететь неожиданный 500 через внешний try/catch.
            log.error("AddBlock: state_last_hash_invalid (login={}, blockchainName={}, serverLastNum={})",
                    login, blockchainName, serverLastNum, e);
            return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "state_last_hash_invalid", serverLastNum, "");
        }

        final String serverLastHashHex = toHex(serverLastHash32);

        // 2) decode block
        final byte[] blockBytes;
        try {
            blockBytes = decodeBase64(blockBytesB64);
        } catch (Exception e) {
            log.warn("AddBlock: некорректный base64 блока (login={}, blockchainName={}, reqGlobalNumber={})",
                    login, blockchainName, globalNumberFromReq, e);
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_base64", serverLastNum, serverLastHashHex);
        }

        // 3) лимит (оставляем как было)
        try {
            long oldSize = st.getFileSizeBytes();
            long limit = st.getSizeLimit();
            long newSize = safeAdd(oldSize, blockBytes.length);

            if (limit > 0 && newSize > limit) {
                log.warn("AddBlock: limit_exceeded (login={}, blockchainName={}, oldSize={}, addLen={}, newSize={}, limit={})",
                        login, blockchainName, oldSize, blockBytes.length, newSize, limit);
                return new AddBlockResult(413, "limit_exceeded", serverLastNum, serverLastHashHex);
            }
        } catch (Exception e) {
            log.error("AddBlock: limit_check_failed (login={}, blockchainName={})", login, blockchainName, e);
            return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "limit_check_failed", serverLastNum, serverLastHashHex);
        }

        // 4) parse block
        final BchBlockEntry block;
        try {
            block = new BchBlockEntry(blockBytes);
        } catch (Exception e) {
            log.warn("AddBlock: не удалось распарсить BchBlockEntry (login={}, blockchainName={}, bytesLen={})",
                    login, blockchainName, blockBytes.length, e);
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_format", serverLastNum, serverLastHashHex);
        }

        // body.check()
        try {
            block.body.check();
        } catch (Exception e) {
            log.warn("AddBlock: body.check() не прошёл (login={}, blockchainName={}, blockNumber={}, type={}, ver={})",
                    login, blockchainName, block.blockNumber, (block.type & 0xFFFF), (block.version & 0xFFFF), e);
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
        }

        // 4.2) запрет дырок: blockNumber строго last+1
        int expectedBlockNumber = serverLastNum + 1;
        if (block.blockNumber != expectedBlockNumber) {
            log.warn("AddBlock: bad_block_number (login={}, blockchainName={}, пришёл={}, ожидали={}, serverLastNum={})",
                    login, blockchainName, block.blockNumber, expectedBlockNumber, serverLastNum);
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_number", serverLastNum, serverLastHashHex);
        }

        // (временная совместимость) req.globalNumber должен совпасть с block.blockNumber
        if (globalNumberFromReq != block.blockNumber) {
            log.warn("AddBlock: req_global_mismatch (login={}, blockchainName={}, reqGlobal={}, blockNumber={})",
                    login, blockchainName, globalNumberFromReq, block.blockNumber);
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "req_global_mismatch", serverLastNum, serverLastHashHex);
        }

        // 4.3) проверка цепочки по prevHash32
        if (!Arrays.equals(block.prevHash32, serverLastHash32)) {
            log.warn("AddBlock: bad_prev_hash (login={}, blockchainName={}, blockNumber={}, clientPrev={}, serverPrev={})",
                    login, blockchainName, block.blockNumber, toHex(block.prevHash32), serverLastHashHex);
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_hash", serverLastNum, serverLastHashHex);
        }

        // 5) pubKey
        final byte[] pubKey32 = st.getBlockchainKeyBytes();
        if (pubKey32 == null || pubKey32.length != 32) {
            log.warn("AddBlock: bad_blockchain_key_len (login={}, blockchainName={}, blockNumber={}, keyLen={})",
                    login, blockchainName, block.blockNumber, (pubKey32 == null ? -1 : pubKey32.length));
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_blockchain_key_len", serverLastNum, serverLastHashHex);
        }

        // 6) подпись по hash32(preimage)
        boolean sigOk;
        try {
            sigOk = BchCryptoVerifier.verifyBlock(block, pubKey32);
        } catch (Exception e) {
            log.warn("AddBlock: signature_verify_failed (login={}, blockchainName={}, blockNumber={})",
                    login, blockchainName, block.blockNumber, e);
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "signature_verify_failed", serverLastNum, serverLastHashHex);
        }

        if (!sigOk) {
            log.warn("AddBlock: bad_signature (login={}, blockchainName={}, blockNumber={})",
                    login, blockchainName, block.blockNumber);
            return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_signature", serverLastNum, serverLastHashHex);
        }

        // 7) line columns (only for BodyHasLine)
        Integer lineCode = null;
        Integer prevLineNumber = null;
        byte[] prevLineHash32 = null;
        Integer thisLineNumber = null;

        if (block.body instanceof BodyHasLine bl) {
            lineCode = bl.lineCode();
            prevLineNumber = bl.prevLineBlockGlobalNumber();
            prevLineHash32 = bl.prevLineBlockHash32();
            thisLineNumber = bl.lineSeq();

            // Нормализация: -1 не пишем в БД (для совместимости со старым TextBody)
            if (prevLineNumber != null && prevLineNumber == -1) {
                prevLineNumber = null;
                prevLineHash32 = null;
                thisLineNumber = null;
            }

            // Если prevLineNumber задан — проверяем его хэш
            if (prevLineNumber != null) {
                try {
                    byte[] dbPrevHash = blocksDAO.getHashByNumber(blockchainName, prevLineNumber);
                    if (dbPrevHash == null) {
                        log.warn("AddBlock: prev_line_block_not_found (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
                                login, blockchainName, block.blockNumber, prevLineNumber);
                        return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "prev_line_block_not_found", serverLastNum, serverLastHashHex);
                    }
                    if (!Arrays.equals(dbPrevHash, require32OrThrow(prevLineHash32, "prevLineHash32 invalid"))) {
                        log.warn("AddBlock: bad_prev_line_hash (login={}, blockchainName={}, blockNumber={}, prevLineNumber={})",
                                login, blockchainName, block.blockNumber, prevLineNumber);
                        return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_prev_line_hash", serverLastNum, serverLastHashHex);
                    }
                } catch (Exception e) {
                    log.error("AddBlock: db_error_prev_line_check (login={}, blockchainName={}, blockNumber={})",
                            login, blockchainName, block.blockNumber, e);
                    return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "db_error_prev_line_check", serverLastNum, serverLastHashHex);
                }
            }
        }

        // 8) сформировать запись и записать (DB + state + файл)
        try {
            BlockEntry be = new BlockEntry();
            be.setLogin(login);
            be.setBchName(blockchainName);

            be.setBlockNumber(block.blockNumber);
            be.setMsgType(block.type & 0xFFFF);
            be.setMsgSubType(block.subType & 0xFFFF);

            be.setBlockBytes(block.toBytes());
            be.setBlockHash(block.getHash32());
            be.setBlockSignature(block.getSignature64());

            // line columns (optional)
            be.setLineCode(lineCode);
            be.setPrevLineNumber(prevLineNumber);
            be.setPrevLineHash(prevLineHash32);
            be.setThisLineNumber(thisLineNumber);

            // target columns (optional)
            if (block.body instanceof BodyHasTarget t) {
                be.setToLogin(t.toLogin());
                be.setToBchName(t.toBchName());
                be.setToBlockNumber(t.toBlockGlobalNumber());
                be.setToBlockHash(t.toBlockHashBytes());
            }

            // edit helper (optional): если TEXT_EDIT_* — это "редактирование блока цели"
            int type = block.type & 0xFFFF;
            int sub = block.subType & 0xFFFF;

            if (type == 1
                    && (sub == (MsgSubType.TEXT_EDIT_POST & 0xFFFF) || sub == (MsgSubType.TEXT_EDIT_REPLY & 0xFFFF))
                    && be.getToBlockNumber() != null) {
                be.setEditedByBlockNumber(be.getToBlockNumber());
            }

            dbWriter.appendBlockAndState(blockchainName, block, st, be);

        } catch (Exception e) {
            log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})",
                    login, blockchainName, block.blockNumber, e);
            return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
        }

        String newHashHex = toHex(block.getHash32());

        log.info("✅ AddBlock ok: login={}, blockchainName={}, blockNumber={}, newHash={}",
                login, blockchainName, block.blockNumber, newHashHex);

        return new AddBlockResult(WireCodes.Status.OK, null, block.blockNumber, newHashHex);
    }

    /* ===================================================================== */
    /* ====================== Helpers ====================================== */
    /* ===================================================================== */

    private static byte[] decodeBase64(String b64) {
        if (b64 == null) throw new IllegalArgumentException("blockBytesB64 == null");
        return Base64Ws.decode(b64);
    }

    private static long safeAdd(long a, long b) {
        long r = a + b;
        if (((a ^ r) & (b ^ r)) < 0) throw new ArithmeticException("long overflow");
        return r;
    }

    private static byte[] require32OrThrow(byte[] b, String msg) {
        if (b == null || b.length != 32) throw new IllegalArgumentException(msg);
        return b;
    }

    private static String toHex(byte[] bytes) {
        if (bytes == null) return "null";
        char[] HEX = "0123456789abcdef".toCharArray();
        char[] out = new char[bytes.length * 2];
        for (int i = 0; i < bytes.length; i++) {
            int v = bytes[i] & 0xFF;
            out[i * 2] = HEX[v >>> 4];
            out[i * 2 + 1] = HEX[v & 0x0F];
        }
        return new String(out);
    }

    /**
     * Спец-ответ ошибки AddBlock: стандартный code/message + поля для ресинка.
     * В wire-формате это окажется внутри payload.
     */
    public static final class AddBlockExceptionResponse extends Net_Exception_Response {
        private Integer serverLastGlobalNumber;
        private String serverLastGlobalHash;

        public Integer getServerLastGlobalNumber() {
            return serverLastGlobalNumber;
        }

        public void setServerLastGlobalNumber(Integer serverLastGlobalNumber) {
            this.serverLastGlobalNumber = serverLastGlobalNumber;
        }

        public String getServerLastGlobalHash() {
            return serverLastGlobalHash;
        }

        public void setServerLastGlobalHash(String serverLastGlobalHash) {
            this.serverLastGlobalHash = serverLastGlobalHash;
        }
    }

    private static final class AddBlockResult {
        final int httpStatus;
        final String reasonCode;
        final int serverLastBlockNumber;
        final String serverLastBlockHashHex;

        AddBlockResult(int httpStatus, String reasonCode, int serverLastBlockNumber, String serverLastBlockHashHex) {
            this.httpStatus = httpStatus;
            this.reasonCode = reasonCode;
            this.serverLastBlockNumber = serverLastBlockNumber;
            this.serverLastBlockHashHex = serverLastBlockHashHex;
        }

        boolean isOk() { return httpStatus == WireCodes.Status.OK; }
    }
}

package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

public final class BlockchainLocks {
    private static final ConcurrentHashMap<String, ReentrantLock> MAP = new ConcurrentHashMap<>();

    private BlockchainLocks() {}

    public static ReentrantLock lockFor(String blockchainName) {
        return MAP.computeIfAbsent(blockchainName, id -> new ReentrantLock(true)); // fair=true
    }
}
package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils;

import blockchain.BchBlockEntry;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.BlockEntry;
import utils.files.FileStoreUtil;

import java.sql.Connection;
import java.sql.SQLException;

/**
 * BlockchainWriter — запись блока в DB + обновление state + запись в файл.
 *
 * ВАЖНО:
 * - Это минимальный рабочий вариант под новый формат.
 * - Если у тебя уже есть "атомарность" сложнее (tmp_bch + commit/recovery) — можно усилить потом.
 */
public final class BlockchainWriter {

    private final BlocksDAO blocksDAO;
    private final BlockchainStateDAO stateDAO;
    private final FileStoreUtil fs = FileStoreUtil.getInstance();

    public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO) {
        this.blocksDAO = blocksDAO;
        this.stateDAO = stateDAO;
    }

    public void appendBlockAndState(String blockchainName,
                                    BchBlockEntry block,
                                    BlockchainStateEntry st,
                                    BlockEntry be) throws SQLException {

        long nowMs = System.currentTimeMillis();

        try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
            c.setAutoCommit(false);
            try {
                // 1) insert block
                blocksDAO.insert(c, be);

                // 2) update state
                st.setLastBlockNumber(block.blockNumber);
                st.setLastBlockHash(block.getHash32());
                st.setFileSizeBytes(st.getFileSizeBytes() + block.toBytes().length);
                st.setUpdatedAtMs(nowMs);

                stateDAO.upsert(c, st);

                c.commit();
            } catch (Exception e) {
                try { c.rollback(); } catch (Exception ignored) {}
                if (e instanceof SQLException se) throw se;
                throw new SQLException("appendBlockAndState failed", e);
            } finally {
                try { c.setAutoCommit(true); } catch (Exception ignored) {}
            }
        }

        // 3) append to file (минимально: просто дописать)
        // Если у тебя уже есть логика tmp_bch+atomicReplace — можно заменить тут.
        String fileName = fs.buildBlockchainFileName(blockchainName);
        fs.addDataToFile(fileName, block.toBytes());
    }
}
package server.logic.ws_protocol.JSON.handlers.connections.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Запрос GetFriendsLists — получить два списка "друзей" по connections_state.
 *
 * {
 *   "op": "GetFriendsLists",
 *   "requestId": "req-100",
 *   "payload": {
 *     "login": "anya"
 *   }
 * }
 *
 * Возвращает:
 *  - out_friends: кому login поставил FRIEND
 *  - in_friends: кто поставил FRIEND этому login
 *
 * ПРО ДОСТУП (на будущее):
 * Сейчас (MVP) без ограничений. Позже можно ограничить видимость связей.
 */
public class Net_GetFriendsLists_Request extends Net_Request {

    private String login;

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }
}
package server.logic.ws_protocol.JSON.handlers.connections.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

import java.util.ArrayList;
import java.util.List;

/**
 * Ответ GetFriendsLists.
 *
 * {
 *   "op": "GetFriendsLists",
 *   "requestId": "req-100",
 *   "status": 200,
 *   "payload": {
 *     "login": "Anya",                  // канонический регистр из БД
 *     "out_friends": ["Bob", "Kate"],   // кому login поставил FRIEND
 *     "in_friends":  ["Alex", "Kate"]   // кто поставил FRIEND login
 *   }
 * }
 */
public class Net_GetFriendsLists_Response extends Net_Response {

    private String login;

    private List<String> out_friends = new ArrayList<>();
    private List<String> in_friends  = new ArrayList<>();

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }

    public List<String> getOut_friends() { return out_friends; }
    public void setOut_friends(List<String> out_friends) { this.out_friends = out_friends; }

    public List<String> getIn_friends() { return in_friends; }
    public void setIn_friends(List<String> in_friends) { this.in_friends = in_friends; }
}
package server.logic.ws_protocol.JSON.handlers.connections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.handlers.connections.entyties.Net_GetFriendsLists_Request;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.MsgSubType;
import shine.db.SqliteDbController;
import shine.db.dao.ConnectionsStateDAO;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.List;

/**
 * GetFriendsLists — получить 2 списка:
 *  - out_friends: кому login поставил FRIEND
 *  - in_friends: кто поставил FRIEND этому login
 *
 * ВАЖНО:
 * - login в запросе может быть любым регистром
 * - в ответе возвращаем канонический регистр (как в solana_users.login)
 *
 * ПРИМЕЧАНИЕ:
 * Таблица пользователей тут названа "solana_users". Если у тебя иначе — поменяй SQL.
 */
public class Net_GetFriendsLists_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_GetFriendsLists_Handler.class);

    @Override
    public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
        Net_GetFriendsLists_Request req = (Net_GetFriendsLists_Request) baseRequest;

        if (req.getLogin() == null || req.getLogin().isBlank()) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_FIELDS",
                    "Некорректные поля: login"
            );
        }

        final String loginAnyCase = req.getLogin().trim();

        try {
            SqliteDbController db = SqliteDbController.getInstance();
            ConnectionsStateDAO dao = ConnectionsStateDAO.getInstance();

            try (Connection c = db.getConnection()) {

                // 1) Канонизируем login через solana_users (NOCASE)
                String canonicalLogin = findCanonicalLogin(c, loginAnyCase);
                if (canonicalLogin == null) {
                    return NetExceptionResponseFactory.error(
                            req,
                            404,
                            "USER_NOT_FOUND",
                            "Пользователь не найден"
                    );
                }

                int relType = (int) MsgSubType.CONNECTION_FRIEND;

                // 2) Два списка (логины канонические)
                List<String> outFriends = dao.listOutgoingByRelTypeCanonical(c, canonicalLogin, relType);
                List<String> inFriends  = dao.listIncomingByRelTypeCanonical(c, canonicalLogin, relType);

                Net_GetFriendsLists_Response resp = new Net_GetFriendsLists_Response();
                resp.setOp(req.getOp());
                resp.setRequestId(req.getRequestId());
                resp.setStatus(WireCodes.Status.OK);

                resp.setLogin(canonicalLogin);
                resp.setOut_friends(outFriends);
                resp.setIn_friends(inFriends);

                return resp;
            }

        } catch (Exception e) {
            log.error("❌ Internal error GetFriendsLists", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.INTERNAL_ERROR,
                    "INTERNAL_ERROR",
                    "Внутренняя ошибка сервера"
            );
        }
    }

    private String findCanonicalLogin(Connection c, String loginAnyCase) throws Exception {
        String sql = """
            SELECT login
            FROM solana_users
            WHERE login = ? COLLATE NOCASE
            LIMIT 1
            """;
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            ps.setString(1, loginAnyCase);
            try (ResultSet rs = ps.executeQuery()) {
                if (!rs.next()) return null;
                return rs.getString("login");
            }
        }
    }
}
package server.logic.ws_protocol.JSON.handlers;

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;

/**
 * Общий интерфейс для всех JSON-хэндлеров.
 */
public interface JsonMessageHandler {

    /**
     * Обработать запрос и вернуть ответ.
     *
     * @param request распарсенный запрос
     * @param ctx     контекст текущего WebSocket-соединения
     */
    Net_Response handle(Net_Request request, ConnectionContext ctx) throws Exception;
}

package server.logic.ws_protocol.JSON.handlers.system.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Ping:
 * {
 *   "op": "Ping",
 *   "requestId": "req-1",
 *   "payload": { "ts": 1700000000000 }
 * }
 *
 * Сервер ничего не проверяет, поле ts можно слать любое.
 */
public class Net_Ping_Request extends Net_Request {

    private long ts;

    public long getTs() { return ts; }
    public void setTs(long ts) { this.ts = ts; }
}
package server.logic.ws_protocol.JSON.handlers.system.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Pong-ответ:
 * {
 *   "op": "Ping",
 *   "requestId": "req-1",
 *   "status": 200,
 *   "payload": { "ts": 1700000000123 }
 * }
 */
public class Net_Ping_Response extends Net_Response {

    private long ts;

    public long getTs() { return ts; }
    public void setTs(long ts) { this.ts = ts; }
}
package server.logic.ws_protocol.JSON.handlers.system;

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.handlers.system.entyties.Net_Ping_Request;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Response;
import server.logic.ws_protocol.WireCodes;

/**
 * Ping — keep-alive.
 * В ответ кладём только ts (текущее время сервера в мс).
 */
public class Net_Ping_Handler implements JsonMessageHandler {

    @Override
    public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
        Net_Ping_Request req = (Net_Ping_Request) baseRequest;

        Net_Ping_Response resp = new Net_Ping_Response();
        resp.setOp(req.getOp());            // "Ping"
        resp.setRequestId(req.getRequestId());
        resp.setStatus(WireCodes.Status.OK);

        // ничего не проверяем, просто отдаём серверное время
        resp.setTs(System.currentTimeMillis());

        return resp;
    }
}
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Запрос AddUser — временная/тестовая регистрация локального пользователя.
 *
 * Клиент отправляет:
 *
 * {
 *   "op": "AddUser",
 *   "requestId": "test-add-1",
 *   "payload": {
 *     "login": "anya",
 *     "blockchainName": "anya-001",
 *     "solanaKey": "base64-ed25519-public-key-login",
 *     "blockchainKey": "base64-ed25519-public-key-blockchain",
 *     "deviceKey": "base64-ed25519-public-key-device",
 *     "bchLimit": 1000000
 *   }
 * }
 *
 * Все поля лежат внутри payload.
 */
public class Net_AddUser_Request extends Net_Request {

    private String login;
    private String blockchainName;

    /** Ключ пользователя Solana (публичный ключ логина) */
    private String solanaKey;

    /** Ключ блокчейна (публичный ключ блокчейна) */
    private String blockchainKey;

    /** Ключ устройства (публичный ключ устройства) */
    private String deviceKey;

    private Integer bchLimit;

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }

    public String getBlockchainName() { return blockchainName; }
    public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }

    public String getSolanaKey() { return solanaKey; }
    public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }

    public String getBlockchainKey() { return blockchainKey; }
    public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }

    public String getDeviceKey() { return deviceKey; }
    public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }

    public Integer getBchLimit() { return bchLimit; }
    public void setBchLimit(Integer bchLimit) { this.bchLimit = bchLimit; }
}
// file: server/logic/ws_protocol/JSON/handlers/tempToTest/entyties/Net_AddUser_Response.java
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Успешный ответ на AddUser.
 *
 * Сейчас дополнительных полей нет — достаточно status=200.
 *
 * Пример:
 * {
 *   "op": "AddUser",
 *   "requestId": "test-add-1",
 *   "status": 200,
 *   "payload": { }
 * }
 */
public class Net_AddUser_Response extends Net_Response {
    // При необходимости сюда можно добавить, например, флаг created/updated и т.п.
}
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Запрос GetUser — проверка/получение пользователя по login.
 *
 * Клиент отправляет:
 *
 * {
 *   "op": "GetUser",
 *   "requestId": "u-1",
 *   "payload": {
 *     "login": "AnYa"
 *   }
 * }
 *
 * Поиск по login выполняется без учёта регистра.
 * В ответе возвращаем login/blockchainName с тем регистром, как в БД.
 */
public class Net_GetUser_Request extends Net_Request {

    private String login;

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }
}
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Ответ GetUser.
 *
 * Всегда status=200.
 *
 * Пример (нет пользователя):
 * {
 *   "op": "GetUser",
 *   "requestId": "u-1",
 *   "status": 200,
 *   "payload": { "exists": false }
 * }
 *
 * Пример (есть пользователь):
 * {
 *   "op": "GetUser",
 *   "requestId": "u-1",
 *   "status": 200,
 *   "payload": {
 *     "exists": true,
 *     "login": "Anya",
 *     "blockchainName": "anya-001",
 *     "solanaKey": "...",
 *     "blockchainKey": "...",
 *     "deviceKey": "..."
 *   }
 * }
 */
public class Net_GetUser_Response extends Net_Response {

    private Boolean exists;

    private String login;
    private String blockchainName;
    private String solanaKey;
    private String blockchainKey;
    private String deviceKey;

    public Boolean getExists() { return exists; }
    public void setExists(Boolean exists) { this.exists = exists; }

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }

    public String getBlockchainName() { return blockchainName; }
    public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; }

    public String getSolanaKey() { return solanaKey; }
    public void setSolanaKey(String solanaKey) { this.solanaKey = solanaKey; }

    public String getBlockchainKey() { return blockchainKey; }
    public void setBlockchainKey(String blockchainKey) { this.blockchainKey = blockchainKey; }

    public String getDeviceKey() { return deviceKey; }
    public void setDeviceKey(String deviceKey) { this.deviceKey = deviceKey; }
}
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Запрос SearchUsers — поиск логинов по префиксу.
 *
 * Клиент отправляет:
 * {
 *   "op": "SearchUsers",
 *   "requestId": "su-1",
 *   "payload": { "prefix": "any" }
 * }
 *
 * Поиск по prefix выполняется без учёта регистра.
 * В ответе возвращаем логины с тем регистром, как в БД.
 */
public class Net_SearchUsers_Request extends Net_Request {

    private String prefix;

    public String getPrefix() { return prefix; }
    public void setPrefix(String prefix) { this.prefix = prefix; }
}
package server.logic.ws_protocol.JSON.handlers.tempToTest.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

import java.util.ArrayList;
import java.util.List;

/**
 * Ответ SearchUsers.
 *
 * Всегда status=200.
 *
 * Пример:
 * {
 *   "op": "SearchUsers",
 *   "requestId": "su-1",
 *   "status": 200,
 *   "payload": {
 *     "logins": ["Anya", "andrew", "Angel"]
 *   }
 * }
 */
public class Net_SearchUsers_Response extends Net_Response {

    private List<String> logins = new ArrayList<>();

    public List<String> getLogins() { return logins; }
    public void setLogins(List<String> logins) { this.logins = logins; }
}
package server.logic.ws_protocol.JSON.handlers.tempToTest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.Base64Ws;
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.handlers.tempToTest.entyties.Net_AddUser_Request;
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.SqliteDbController;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.SolanaUserEntry;
import utils.blockchain.BlockchainNameUtil;

import java.sql.Connection;
import java.sql.SQLException;

public class Net_AddUser_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_AddUser_Handler.class);

    /** TEST ONLY */
    private static final int TEST_BCH_LIMIT = 1_000_000;

    @Override
    public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
        Net_AddUser_Request req = (Net_AddUser_Request) baseRequest;

        if (req.getLogin() == null || req.getLogin().isBlank()
                || req.getBlockchainName() == null || req.getBlockchainName().isBlank()
                || req.getSolanaKey() == null || req.getSolanaKey().isBlank()
                || req.getBlockchainKey() == null || req.getBlockchainKey().isBlank()
                || req.getDeviceKey() == null || req.getDeviceKey().isBlank()) {

            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_FIELDS",
                    "Некорректные поля: login/blockchainName/solanaKey/blockchainKey/deviceKey"
            );
        }

        // blockchainName должен быть вида: <login>-NNN
        if (!BlockchainNameUtil.isBlockchainNameMatchesLogin(req.getBlockchainName(), req.getLogin())) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_BLOCKCHAIN_NAME",
                    "blockchainName должен быть вида <login>-NNN (пример: anya-001)"
            );
        }

        int limit = (req.getBchLimit() == null || req.getBchLimit() <= 0)
                ? TEST_BCH_LIMIT
                : req.getBchLimit();

        try {
            // базовая валидация форматов ключей: Base64(32 bytes)
            byte[] solanaKey32;
            byte[] blockchainKey32;
            byte[] deviceKey32;

            try {
                solanaKey32 = Base64Ws.decodeLen(req.getSolanaKey(), 32, "solanaKey");
                blockchainKey32 = Base64Ws.decodeLen(req.getBlockchainKey(), 32, "blockchainKey");
                deviceKey32 = Base64Ws.decodeLen(req.getDeviceKey(), 32, "deviceKey");
            } catch (IllegalArgumentException e) {
                return NetExceptionResponseFactory.error(
                        req,
                        WireCodes.Status.BAD_REQUEST,
                        "BAD_KEY_FORMAT",
                        e.getMessage()
                );
            }

            // (переменные не используются дальше, но оставляем для ясности проверки длины)
            if (solanaKey32.length != 32 || blockchainKey32.length != 32 || deviceKey32.length != 32) {
                return NetExceptionResponseFactory.error(
                        req,
                        WireCodes.Status.BAD_REQUEST,
                        "BAD_KEY_FORMAT",
                        "solanaKey/blockchainKey/deviceKey должны быть Base64(32 bytes)"
                );
            }

            SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
            BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();

            SqliteDbController db = SqliteDbController.getInstance();

            try (Connection c = db.getConnection()) {
                c.setAutoCommit(false);

                // 1. Проверяем, что пользователя нет (case-insensitive)
                if (usersDAO.getByLogin(c, req.getLogin()) != null) {
                    return NetExceptionResponseFactory.error(
                            req,
                            409,
                            "USER_ALREADY_EXISTS",
                            "Пользователь с таким login уже существует"
                    );
                }

                // 2. Проверяем, что blockchainName ещё нет (case-sensitive, как в БД)
                if (usersDAO.existsByBlockchainName(c, req.getBlockchainName())) {
                    return NetExceptionResponseFactory.error(
                            req,
                            409,
                            "BLOCKCHAIN_ALREADY_EXISTS",
                            "Пользователь с таким blockchainName уже существует"
                    );
                }

                // 3. На всякий случай оставляем старую проверку blockchain_state,
                //    потому что эта таблица нужна серверу (состояние цепочки/лимиты).
                if (stateDAO.getByBlockchainName(c, req.getBlockchainName()) != null) {
                    return NetExceptionResponseFactory.error(
                            req,
                            409,
                            "BLOCKCHAIN_STATE_ALREADY_EXISTS",
                            "blockchain_state уже существует"
                    );
                }

                // 4. Создаём пользователя (все поля теперь лежат в solana_users)
                SolanaUserEntry user = new SolanaUserEntry();
                user.setLogin(req.getLogin());
                user.setBlockchainName(req.getBlockchainName());
                user.setSolanaKey(req.getSolanaKey());
                user.setBlockchainKey(req.getBlockchainKey());
                user.setDeviceKey(req.getDeviceKey());

                usersDAO.insert(c, user);

                // 5. Создаём INITIAL blockchain_state (для работы сервера)
                BlockchainStateEntry st = new BlockchainStateEntry();
                st.setBlockchainName(req.getBlockchainName());
                st.setLogin(req.getLogin());
                st.setBlockchainKey(req.getBlockchainKey()); // Base64(32)
                st.setLastBlockNumber(-1);
                st.setLastBlockHash(new byte[32]);
                st.setFileSizeBytes(0);
                st.setSizeLimit(limit);
                st.setUpdatedAtMs(System.currentTimeMillis());

                stateDAO.upsert(c, st);

                c.commit();
            }

            Net_AddUser_Response resp = new Net_AddUser_Response();
            resp.setOp(req.getOp());
            resp.setRequestId(req.getRequestId());
            resp.setStatus(WireCodes.Status.OK);

            log.info("✅ AddUser ok: login={}, blockchainName={}, limit={}",
                    req.getLogin(), req.getBlockchainName(), limit);

            return resp;

        } catch (SQLException e) {
            log.error("❌ DB error AddUser", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "DB_ERROR",
                    "Ошибка БД"
            );
        } catch (Exception e) {
            log.error("❌ Internal error AddUser", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.INTERNAL_ERROR,
                    "INTERNAL_ERROR",
                    "Внутренняя ошибка сервера"
            );
        }
    }
}
package server.logic.ws_protocol.JSON.handlers.tempToTest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.handlers.tempToTest.entyties.Net_GetUser_Request;
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.SolanaUserEntry;

import java.sql.SQLException;

public class Net_GetUser_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_GetUser_Handler.class);

    @Override
    public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
        Net_GetUser_Request req = (Net_GetUser_Request) baseRequest;

        if (req.getLogin() == null || req.getLogin().isBlank()) {
            // тут логичнее BAD_REQUEST, но ты просил: "нет пользователя" тоже 200.
            // Поэтому BAD_REQUEST оставляем только на реально пустой login.
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_FIELDS",
                    "Некорректные поля: login"
            );
        }

        SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();

        try {
            SolanaUserEntry u = usersDAO.getByLogin(req.getLogin());

            Net_GetUser_Response resp = new Net_GetUser_Response();
            resp.setOp(req.getOp());
            resp.setRequestId(req.getRequestId());
            resp.setStatus(WireCodes.Status.OK);

            if (u == null) {
                resp.setExists(false);
                log.info("ℹ️ GetUser: not found for login={}", req.getLogin());
                return resp;
            }

            // ВАЖНО:
            // - Поиск по login был case-insensitive,
            // - а тут возвращаем login/blockchainName как в БД (с исходным регистром).
            resp.setExists(true);
            resp.setLogin(u.getLogin());
            resp.setBlockchainName(u.getBlockchainName());
            resp.setSolanaKey(u.getSolanaKey());
            resp.setBlockchainKey(u.getBlockchainKey());
            resp.setDeviceKey(u.getDeviceKey());

            log.info("✅ GetUser: found login={}, blockchainName={}", u.getLogin(), u.getBlockchainName());
            return resp;

        } catch (SQLException e) {
            log.error("❌ DB error GetUser", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "DB_ERROR",
                    "Ошибка БД"
            );
        } catch (Exception e) {
            log.error("❌ Internal error GetUser", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.INTERNAL_ERROR,
                    "INTERNAL_ERROR",
                    "Внутренняя ошибка сервера"
            );
        }
    }
}
package server.logic.ws_protocol.JSON.handlers.tempToTest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.handlers.tempToTest.entyties.Net_SearchUsers_Request;
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.SolanaUsersDAO;
import shine.db.entities.SolanaUserEntry;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class Net_SearchUsers_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_SearchUsers_Handler.class);

    @Override
    public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
        Net_SearchUsers_Request req = (Net_SearchUsers_Request) baseRequest;

        if (req.getPrefix() == null || req.getPrefix().isBlank()) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_FIELDS",
                    "Некорректные поля: prefix"
            );
        }

        String prefix = req.getPrefix().trim();

        try {
            SolanaUsersDAO dao = SolanaUsersDAO.getInstance();
            List<SolanaUserEntry> users = dao.searchByLoginPrefix(prefix); // case-insensitive + LIMIT 5

            List<String> logins = new ArrayList<>();
            for (SolanaUserEntry u : users) {
                if (u != null && u.getLogin() != null) {
                    logins.add(u.getLogin()); // регистр как в БД
                }
            }

            Net_SearchUsers_Response resp = new Net_SearchUsers_Response();
            resp.setOp(req.getOp());
            resp.setRequestId(req.getRequestId());
            resp.setStatus(WireCodes.Status.OK);
            resp.setLogins(logins);

            log.info("✅ SearchUsers ok: prefix='{}' -> {}", prefix, logins.size());
            return resp;

        } catch (SQLException e) {
            log.error("❌ DB error SearchUsers", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "DB_ERROR",
                    "Ошибка БД"
            );
        } catch (Exception e) {
            log.error("❌ Internal error SearchUsers", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.INTERNAL_ERROR,
                    "INTERNAL_ERROR",
                    "Внутренняя ошибка сервера"
            );
        }
    }
}
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Запрос GetUserParam — получить один параметр пользователя.
 *
 * {
 *   "op": "GetUserParam",
 *   "requestId": "req-1",
 *   "payload": {
 *     "login": "anya",
 *     "param": "feed:lastSeenGlobal"
 *   }
 * }
 *
 * ПРО ДОСТУП (на будущее):
 * ---------------------------------------------------------------------------------
 * Сейчас (MVP) этот запрос не ограничивает просмотр параметров, т.к. проект в тестовом режиме.
 * Позже, вероятно, потребуется ограничить: кто и какие параметры может читать (сессия/права).
 * Но для MVP эти проверки не нужны.
 * ---------------------------------------------------------------------------------
 */
public class Net_GetUserParam_Request extends Net_Request {

    private String login;
    private String param;

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }

    public String getParam() { return param; }
    public void setParam(String param) { this.param = param; }
}
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Ответ GetUserParam.
 *
 * Если найден:
 * {
 *   "op": "GetUserParam",
 *   "requestId": "req-1",
 *   "status": 200,
 *   "payload": {
 *     "login": "anya",
 *     "param": "feed:lastSeenGlobal",
 *     "time_ms": 1736000000123,
 *     "value": "105",
 *     "device_key": "base64-32",
 *     "signature": "base64-64"
 *   }
 * }
 *
 * Если не найден:
 * status=404, payload пустой.
 */
public class Net_GetUserParam_Response extends Net_Response {

    private String login;
    private String param;
    private Long time_ms;
    private String value;
    private String device_key;
    private String signature;

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }

    public String getParam() { return param; }
    public void setParam(String param) { this.param = param; }

    public Long getTime_ms() { return time_ms; }
    public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }

    public String getValue() { return value; }
    public void setValue(String value) { this.value = value; }

    public String getDevice_key() { return device_key; }
    public void setDevice_key(String device_key) { this.device_key = device_key; }

    public String getSignature() { return signature; }
    public void setSignature(String signature) { this.signature = signature; }
}
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Запрос ListUserParams — получить все сохранённые параметры пользователя.
 *
 * {
 *   "op": "ListUserParams",
 *   "requestId": "req-2",
 *   "payload": {
 *     "login": "anya"
 *   }
 * }
 *
 * ПРО ДОСТУП (на будущее):
 * ---------------------------------------------------------------------------------
 * Сейчас (MVP) запрос не ограничивает просмотр параметров.
 * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
 * Для MVP эти проверки не нужны.
 * ---------------------------------------------------------------------------------
 */
public class Net_ListUserParams_Request extends Net_Request {

    private String login;

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }
}
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

import java.util.ArrayList;
import java.util.List;

/**
 * Ответ ListUserParams — список всех параметров пользователя.
 *
 * {
 *   "op": "ListUserParams",
 *   "requestId": "req-2",
 *   "status": 200,
 *   "payload": {
 *     "login": "anya",
 *     "params": [
 *       {
 *         "login": "anya",
 *         "param": "feed:lastSeenGlobal",
 *         "time_ms": 1736000000123,
 *         "value": "105",
 *         "device_key": "base64-32",
 *         "signature": "base64-64"
 *       },
 *       ...
 *     ]
 *   }
 * }
 */
public class Net_ListUserParams_Response extends Net_Response {

    private String login;
    private List<Item> params = new ArrayList<>();

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }

    public List<Item> getParams() { return params; }
    public void setParams(List<Item> params) { this.params = params; }

    public static class Item {
        private String login;
        private String param;
        private Long time_ms;
        private String value;
        private String device_key;
        private String signature;

        public String getLogin() { return login; }
        public void setLogin(String login) { this.login = login; }

        public String getParam() { return param; }
        public void setParam(String param) { this.param = param; }

        public Long getTime_ms() { return time_ms; }
        public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }

        public String getValue() { return value; }
        public void setValue(String value) { this.value = value; }

        public String getDevice_key() { return device_key; }
        public void setDevice_key(String device_key) { this.device_key = device_key; }

        public String getSignature() { return signature; }
        public void setSignature(String signature) { this.signature = signature; }
    }
}
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Запрос UpsertUserParam — добавить/обновить сохранённый параметр пользователя.
 *
 * Клиент отправляет:
 *
 * {
 *   "op": "UpsertUserParam",
 *   "requestId": "req-123",
 *   "payload": {
 *     "login": "anya",
 *     "param": "feed:lastSeenGlobal",
 *     "time_ms": 1736000000123,
 *     "value": "105",
 *     "device_key": "base64-ed25519-public-key-32",
 *     "signature": "base64-ed25519-signature-64"
 *   }
 * }
 *
 * Подпись считается от UTF-8 строки:
 *   USER_PARAMETER_PREFIX + login + param + time_ms + value
 */
public class Net_UpsertUserParam_Request extends Net_Request {

    private String login;
    private String param;
    private Long time_ms;
    private String value;

    private String device_key;
    private String signature;

    public String getLogin() { return login; }
    public void setLogin(String login) { this.login = login; }

    public String getParam() { return param; }
    public void setParam(String param) { this.param = param; }

    public Long getTime_ms() { return time_ms; }
    public void setTime_ms(Long time_ms) { this.time_ms = time_ms; }

    public String getValue() { return value; }
    public void setValue(String value) { this.value = value; }

    public String getDevice_key() { return device_key; }
    public void setDevice_key(String device_key) { this.device_key = device_key; }

    public String getSignature() { return signature; }
    public void setSignature(String signature) { this.signature = signature; }
}
package server.logic.ws_protocol.JSON.handlers.userParams.entyties;

import server.logic.ws_protocol.JSON.entyties.Net_Response;

/**
 * Ответ на UpsertUserParam.
 *
 * Успех:
 * {
 *   "op": "UpsertUserParam",
 *   "requestId": "req-123",
 *   "status": 200,
 *   "payload": { }
 * }
 */
public class Net_UpsertUserParam_Response extends Net_Response {
    // MVP: без payload. При желании позже можно добавить created/updated.
}
package server.logic.ws_protocol.JSON.handlers.userParams;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.handlers.userParams.entyties.Net_GetUserParam_Request;
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.SqliteDbController;
import shine.db.dao.UserParamsDAO;
import shine.db.entities.UserParamEntry;

import java.sql.Connection;

/**
 * GetUserParam — получить один параметр пользователя.
 *
 * ПРО ДОСТУП (на будущее):
 * ---------------------------------------------------------------------------------
 * Сейчас (MVP) запрос не ограничивает просмотр параметров.
 * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
 * Для MVP эти проверки не нужны.
 * ---------------------------------------------------------------------------------
 */
public class Net_GetUserParam_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_GetUserParam_Handler.class);

    @Override
    public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
        Net_GetUserParam_Request req = (Net_GetUserParam_Request) baseRequest;

        if (req.getLogin() == null || req.getLogin().isBlank()
                || req.getParam() == null || req.getParam().isBlank()) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_FIELDS",
                    "Некорректные поля: login/param"
            );
        }

        String login = req.getLogin().trim();
        String param = req.getParam().trim();

        try {
            SqliteDbController db = SqliteDbController.getInstance();
            UserParamsDAO dao = UserParamsDAO.getInstance();

            try (Connection c = db.getConnection()) {
                UserParamEntry e = dao.getByLoginAndParam(c, login, param);

                if (e == null) {
                    Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
                    resp.setOp(req.getOp());
                    resp.setRequestId(req.getRequestId());
                    resp.setStatus(404);
                    return resp;
                }

                Net_GetUserParam_Response resp = new Net_GetUserParam_Response();
                resp.setOp(req.getOp());
                resp.setRequestId(req.getRequestId());
                resp.setStatus(WireCodes.Status.OK);

                resp.setLogin(e.getLogin());
                resp.setParam(e.getParam());
                resp.setTime_ms(e.getTimeMs());
                resp.setValue(e.getValue());
                resp.setDevice_key(e.getDeviceKey());
                resp.setSignature(e.getSignature());

                return resp;
            }

        } catch (Exception e) {
            log.error("❌ Internal error GetUserParam", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.INTERNAL_ERROR,
                    "INTERNAL_ERROR",
                    "Внутренняя ошибка сервера"
            );
        }
    }
}
package server.logic.ws_protocol.JSON.handlers.userParams;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.handlers.userParams.entyties.Net_ListUserParams_Request;
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.SqliteDbController;
import shine.db.dao.UserParamsDAO;
import shine.db.entities.UserParamEntry;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;

/**
 * ListUserParams — получить все параметры пользователя.
 *
 * ПРО ДОСТУП (на будущее):
 * ---------------------------------------------------------------------------------
 * Сейчас (MVP) запрос не ограничивает просмотр параметров.
 * В будущем, вероятно, потребуется проверка сессии/прав: кто может читать параметры.
 * Для MVP эти проверки не нужны.
 * ---------------------------------------------------------------------------------
 */
public class Net_ListUserParams_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_ListUserParams_Handler.class);

    @Override
    public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
        Net_ListUserParams_Request req = (Net_ListUserParams_Request) baseRequest;

        if (req.getLogin() == null || req.getLogin().isBlank()) {
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_FIELDS",
                    "Некорректные поля: login"
            );
        }

        String login = req.getLogin().trim();

        try {
            SqliteDbController db = SqliteDbController.getInstance();
            UserParamsDAO dao = UserParamsDAO.getInstance();

            List<UserParamEntry> entries;
            try (Connection c = db.getConnection()) {
                entries = dao.getByLogin(c, login);
            }

            Net_ListUserParams_Response resp = new Net_ListUserParams_Response();
            resp.setOp(req.getOp());
            resp.setRequestId(req.getRequestId());
            resp.setStatus(WireCodes.Status.OK);

            resp.setLogin(login);

            List<Net_ListUserParams_Response.Item> items = new ArrayList<>();
            for (UserParamEntry e : entries) {
                Net_ListUserParams_Response.Item it = new Net_ListUserParams_Response.Item();
                it.setLogin(e.getLogin());
                it.setParam(e.getParam());
                it.setTime_ms(e.getTimeMs());
                it.setValue(e.getValue());
                it.setDevice_key(e.getDeviceKey());
                it.setSignature(e.getSignature());
                items.add(it);
            }
            resp.setParams(items);

            return resp;

        } catch (Exception e) {
            log.error("❌ Internal error ListUserParams", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.INTERNAL_ERROR,
                    "INTERNAL_ERROR",
                    "Внутренняя ошибка сервера"
            );
        }
    }
}
package server.logic.ws_protocol.JSON.handlers.userParams;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.Base64Ws;
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.handlers.userParams.entyties.Net_UpsertUserParam_Request;
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.SqliteDbController;
import shine.db.dao.SolanaUsersDAO;
import shine.db.dao.UserParamsDAO;
import shine.db.entities.SolanaUserEntry;
import shine.db.entities.UserParamEntry;
import utils.config.ShineSignatureConstants;
import utils.crypto.Ed25519Util;

import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.SQLException;

/**
 * Net_UpsertUserParam_Handler
 *
 * Делает (MVP, без "сессий"):
 *  1) Проверка входных полей.
 *  2) Проверка подписи Ed25519 по device_key.
 *  3) Проверка, что пользователь существует и что device_key принадлежит этому login.
 *  4) Атомарная запись в БД "только если time_ms новее" (UPSERT + WHERE).
 *
 * ВАЖНО:
 *  - НИКАКИХ ручных транзакций / BEGIN здесь нет.
 *  - autoCommit=true, каждый statement завершённый сам по себе.
 *  - Гонки не страшны: если за время проверок кто-то записал более новый time_ms,
 *    наш финальный UPSERT просто вернёт 0 обновлённых строк.
 */
public class Net_UpsertUserParam_Handler implements JsonMessageHandler {

    private static final Logger log = LoggerFactory.getLogger(Net_UpsertUserParam_Handler.class);

    @Override
    public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
        Net_UpsertUserParam_Request req = (Net_UpsertUserParam_Request) baseRequest;

        if (req.getLogin() == null || req.getLogin().isBlank()
                || req.getParam() == null || req.getParam().isBlank()
                || req.getTime_ms() == null || req.getTime_ms() <= 0
                || req.getValue() == null
                || req.getDevice_key() == null || req.getDevice_key().isBlank()
                || req.getSignature() == null || req.getSignature().isBlank()) {

            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.BAD_REQUEST,
                    "BAD_FIELDS",
                    "Некорректные поля: login/param/time_ms/value/device_key/signature"
            );
        }

        final String login = req.getLogin().trim();
        final String param = req.getParam().trim();
        final long timeMs = req.getTime_ms();
        final String value = req.getValue();
        final String deviceKeyB64 = req.getDevice_key().trim();
        final String signatureB64 = req.getSignature().trim();

        try {
            // ---------------- Base64 decode ----------------
            byte[] pubKey32;
            byte[] sig64;
            try {
                pubKey32 = Base64Ws.decodeLen(deviceKeyB64, 32, "device_key");
                sig64 = Base64Ws.decodeLen(signatureB64, 64, "signature");
            } catch (IllegalArgumentException e) {
                return NetExceptionResponseFactory.error(
                        req,
                        WireCodes.Status.BAD_REQUEST,
                        "BAD_BASE64",
                        "device_key/signature должны быть Base64"
                );
            }

            // ---------------- Signature verify ----------------
            String signText = ShineSignatureConstants.USER_PARAMETER_PREFIX
                    + login
                    + param
                    + timeMs
                    + value;

            byte[] signBytes = signText.getBytes(StandardCharsets.UTF_8);

            boolean sigOk = Ed25519Util.verify(signBytes, sig64, pubKey32);
            if (!sigOk) {
                return NetExceptionResponseFactory.error(
                        req,
                        403,
                        "SIGNATURE_INVALID",
                        "Подпись не прошла проверку"
                );
            }

            // ---------------- DB checks + upsert ----------------
            SqliteDbController db = SqliteDbController.getInstance();
            SolanaUsersDAO usersDAO = SolanaUsersDAO.getInstance();
            UserParamsDAO paramsDAO = UserParamsDAO.getInstance();

            try (Connection c = db.getConnection()) {
                // 1) user exists
                SolanaUserEntry user = usersDAO.getByLogin(c, login);
                if (user == null) {
                    return NetExceptionResponseFactory.error(
                            req,
                            404,
                            "USER_NOT_FOUND",
                            "Пользователь не найден"
                    );
                }

                // 2) device key must match the user's stored deviceKey
                String userDeviceKey = user.getDeviceKey();
                if (userDeviceKey == null || userDeviceKey.isBlank()) {
                    return NetExceptionResponseFactory.error(
                            req,
                            WireCodes.Status.SERVER_DATA_ERROR,
                            "USER_DEVICE_KEY_EMPTY",
                            "У пользователя не задан deviceKey в БД"
                    );
                }

                if (!userDeviceKey.trim().equals(deviceKeyB64)) {
                    return NetExceptionResponseFactory.error(
                            req,
                            403,
                            "DEVICE_KEY_MISMATCH",
                            "device_key не соответствует пользователю"
                    );
                }

                // 3) atomic upsert-if-newer
                UserParamEntry e = new UserParamEntry(
                        login,
                        param,
                        timeMs,
                        value,
                        deviceKeyB64,
                        signatureB64
                );

                int changed = paramsDAO.upsertIfNewer(c, e);

                Net_UpsertUserParam_Response resp = new Net_UpsertUserParam_Response();
                resp.setOp(req.getOp());
                resp.setRequestId(req.getRequestId());
                resp.setStatus(WireCodes.Status.OK);

                if (changed == 1) {
                    log.info("✅ UpsertUserParam applied: login={}, param={}, time_ms={}", login, param, timeMs);
                } else {
                    // 0 строк — значит в БД уже есть time_ms >= incoming
                    log.info("ℹ️ UpsertUserParam ignored (not newer): login={}, param={}, time_ms={}", login, param, timeMs);
                }

                return resp;
            }

        } catch (SQLException e) {
            log.error("❌ DB error UpsertUserParam", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.SERVER_DATA_ERROR,
                    "DB_ERROR",
                    "Ошибка БД"
            );
        } catch (Exception e) {
            log.error("❌ Internal error UpsertUserParam", e);
            return NetExceptionResponseFactory.error(
                    req,
                    WireCodes.Status.INTERNAL_ERROR,
                    "INTERNAL_ERROR",
                    "Внутренняя ошибка сервера"
            );
        }
    }
}
package server.logic.ws_protocol.JSON;

import server.logic.ws_protocol.JSON.entyties.Net_Request;
import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;

import server.logic.ws_protocol.JSON.handlers.auth.Net_AuthChallenge_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_CloseActiveSession_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_CreateAuthSession__Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_ListSessions_Handler;

// --- NEW v2 session login ---
import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionChallenge_Handler;
import server.logic.ws_protocol.JSON.handlers.auth.Net_SessionLogin_Handler;

// --- auth entities ---
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_AuthChallenge_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CreateAuthSession_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_ListSessions_Request;

// --- NEW v2 entities ---
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionChallenge_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_SessionLogin_Request;

import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler;
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;

import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_AddUser_Handler;
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_AddUser_Request;

import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_GetUser_Handler;
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_GetUser_Request;

// --- NEW: SearchUsers ---
import server.logic.ws_protocol.JSON.handlers.tempToTest.Net_SearchUsers_Handler;
import server.logic.ws_protocol.JSON.handlers.tempToTest.entyties.Net_SearchUsers_Request;

import server.logic.ws_protocol.JSON.handlers.userParams.Net_GetUserParam_Handler;
import server.logic.ws_protocol.JSON.handlers.userParams.Net_ListUserParams_Handler;
import server.logic.ws_protocol.JSON.handlers.userParams.Net_UpsertUserParam_Handler;
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_GetUserParam_Request;
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_ListUserParams_Request;
import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUserParam_Request;

// --- NEW: connections friends lists ---
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request;

// --- NEW: Ping ---
import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler;
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_Ping_Request;

import java.util.Map;

/**
 * JsonHandlerRegistry — единое место, где руками регистрируются
 * JSON-операции: op → handler и op → requestClass.
 */
public final class JsonHandlerRegistry {

    private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
            Map.entry("AddUser",            new Net_AddUser_Handler()),
            Map.entry("GetUser",            new Net_GetUser_Handler()),
            Map.entry("SearchUsers",        new Net_SearchUsers_Handler()),

            // --- auth ---
            Map.entry("AuthChallenge",      new Net_AuthChallenge_Handler()),
            Map.entry("CreateAuthSession",  new Net_CreateAuthSession__Handler()),
            Map.entry("CloseActiveSession", new Net_CloseActiveSession_Handler()),
            Map.entry("ListSessions",       new Net_ListSessions_Handler()),

            // --- login to existing session in 2 steps ---
            Map.entry("SessionChallenge",   new Net_SessionChallenge_Handler()),
            Map.entry("SessionLogin",       new Net_SessionLogin_Handler()),

            // --- blockchain ---
            Map.entry("AddBlock",           new Net_AddBlock_Handler()),

            // --- userParams ---
            Map.entry("UpsertUserParam",    new Net_UpsertUserParam_Handler()),
            Map.entry("GetUserParam",       new Net_GetUserParam_Handler()),
            Map.entry("ListUserParams",     new Net_ListUserParams_Handler()),

            // --- connections ---
            Map.entry("GetFriendsLists",    new Net_GetFriendsLists_Handler()),

            // --- system ---
            Map.entry("Ping",               new Net_Ping_Handler())

            // --- subscriptions ---
//            Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
    );

    private static final Map<String, Class<? extends Net_Request>> REQUEST_TYPES = Map.ofEntries(
            Map.entry("AddUser",            Net_AddUser_Request.class),
            Map.entry("GetUser",            Net_GetUser_Request.class),
            Map.entry("SearchUsers",        Net_SearchUsers_Request.class),

            // --- auth ---
            Map.entry("AuthChallenge",      Net_AuthChallenge_Request.class),
            Map.entry("CreateAuthSession",  Net_CreateAuthSession_Request.class),
            Map.entry("CloseActiveSession", Net_CloseActiveSession_Request.class),
            Map.entry("ListSessions",       Net_ListSessions_Request.class),

            // --- NEW v2 ---
            Map.entry("SessionChallenge",   Net_SessionChallenge_Request.class),
            Map.entry("SessionLogin",       Net_SessionLogin_Request.class),

            // --- blockchain ---
            Map.entry("AddBlock",           Net_AddBlock_Request.class),

            // --- userParams ---
            Map.entry("UpsertUserParam",    Net_UpsertUserParam_Request.class),
            Map.entry("GetUserParam",       Net_GetUserParam_Request.class),
            Map.entry("ListUserParams",     Net_ListUserParams_Request.class),

            // --- connections ---
            Map.entry("GetFriendsLists",    Net_GetFriendsLists_Request.class),

            // --- system ---
            Map.entry("Ping",               Net_Ping_Request.class)
    );

    private JsonHandlerRegistry() { }

    public static Map<String, JsonMessageHandler> getHandlers() {
        return HANDLERS;
    }

    public static Map<String, Class<? extends Net_Request>> getRequestTypes() {
        return REQUEST_TYPES;
    }
}
package server.logic.ws_protocol.JSON;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.entyties.Net_Exception_Response;
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.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;

import java.util.Map;

/**
 * JsonInboundProcessor — обработка JSON-сообщений.
 *
 * 1) Парсит общий пакет (op, requestId, payload).
 * 2) По op выбирает класс запроса и хэндлер.
 * 3) Собирает "плоский" объект: op + requestId + поля из payload.
 * 4) Маппит его в NetRequest через ObjectMapper.
 * 5) Вызывает хэндлер, получает NetResponse.
 * 6) Собирает JSON-ответ:
 *    {
 *      "op": ...,
 *      "requestId": ...,
 *      "status": ...,
 *      "payload": { все поля response, кроме op/requestId/status/payload }
 *    }
 */
public final class JsonInboundProcessor {

    private static final Logger log = LoggerFactory.getLogger(JsonInboundProcessor.class);

    private static final ObjectMapper JSON_MAPPER = new ObjectMapper()
            .setSerializationInclusion(JsonInclude.Include.NON_NULL);

    private static final Map<String, JsonMessageHandler> JSON_HANDLERS =
            JsonHandlerRegistry.getHandlers();

    private static final Map<String, Class<? extends Net_Request>> JSON_REQUEST_TYPES =
            JsonHandlerRegistry.getRequestTypes();

    private JsonInboundProcessor() {
        // utility
    }

    public static String processJson(String json, ConnectionContext ctx) {
        String op = null;
        String requestId = null;

        // Для лога полезно знать, кто прислал (хотя бы login/sessionId, если есть)
        String ctxLogin = safe(ctx != null ? ctx.getLogin() : null);
        String ctxSessionId = safe(ctx != null ? ctx.getSessionId() : null);

        try {
            if (json == null || json.isBlank()) {
                Net_Exception_Response err = NetExceptionResponseFactory.error(
                        null,
                        null,
                        WireCodes.Status.BAD_REQUEST,
                        "EMPTY_JSON",
                        "Пустое JSON-сообщение"
                );

                String out = writeResponse(err);

                // DEBUG: что пришло / что ушло
                if (log.isDebugEnabled()) {
                    log.debug("JSON IN  (login={}, sessionId={}): <empty>", ctxLogin, ctxSessionId);
                    log.debug("JSON OUT (login={}, sessionId={}): {}", ctxLogin, ctxSessionId, shorten(out, 1200));
                }
                return out;
            }

            // DEBUG: сырой вход (обрезаем, чтобы не убить лог)
            if (log.isDebugEnabled()) {
                log.debug("JSON IN  (login={}, sessionId={}): {}", ctxLogin, ctxSessionId, shorten(json, 1200));
            }

            // 1) Парсим общий пакет
            JsonNode root = JSON_MAPPER.readTree(json);

            // 2) op и requestId из корня
            op = getTextOrNull(root, "op");
            requestId = getTextOrNull(root, "requestId");

            if (op == null || op.isEmpty()) {
                Net_Exception_Response err = NetExceptionResponseFactory.error(
                        null,
                        requestId,
                        WireCodes.Status.BAD_REQUEST,
                        "NO_OP",
                        "Поле 'op' отсутствует или пустое"
                );

                String out = writeResponse(err);
                if (log.isDebugEnabled()) {
                    log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
                            ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
                }
                return out;
            }

            JsonMessageHandler handler = JSON_HANDLERS.get(op);
            Class<? extends Net_Request> reqClass = JSON_REQUEST_TYPES.get(op);

            if (handler == null || reqClass == null) {
                Net_Exception_Response err = NetExceptionResponseFactory.error(
                        op,
                        requestId,
                        WireCodes.Status.BAD_REQUEST,
                        "UNKNOWN_OP",
                        "Неизвестная операция: " + op
                );

                String out = writeResponse(err);
                if (log.isDebugEnabled()) {
                    log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
                            ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
                }
                return out;
            }

            // 3) Берём payload
            JsonNode payloadNode = root.get("payload");
            if (payloadNode == null || payloadNode.isNull()) {
                Net_Exception_Response err = NetExceptionResponseFactory.error(
                        op,
                        requestId,
                        WireCodes.Status.BAD_REQUEST,
                        "NO_PAYLOAD",
                        "Поле 'payload' отсутствует"
                );

                String out = writeResponse(err);
                if (log.isDebugEnabled()) {
                    log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
                            ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
                }
                return out;
            }
            if (!payloadNode.isObject()) {
                Net_Exception_Response err = NetExceptionResponseFactory.error(
                        op,
                        requestId,
                        WireCodes.Status.BAD_REQUEST,
                        "BAD_PAYLOAD",
                        "Поле 'payload' должно быть объектом"
                );

                String out = writeResponse(err);
                if (log.isDebugEnabled()) {
                    log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
                            ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
                }
                return out;
            }

            // 3.1 Собираем "плоский" объект для маппинга в NetRequest:
            //     op + requestId + поля из payload
            ObjectNode merged = JSON_MAPPER.createObjectNode();

            // Добавляем op и requestId, чтобы они попали в NetRequest
            merged.put("op", op);
            if (requestId != null) merged.put("requestId", requestId);

            // Добавляем все поля из payload внутрь
            merged.setAll((ObjectNode) payloadNode);

            // 4) Маппим в конкретный класс NetRequest
            Net_Request request;
            try {
                request = JSON_MAPPER.treeToValue(merged, reqClass);
            } catch (Exception mapErr) {
                // Важно: вот это часто “теряется”, если не логировать отдельно
                log.error("❌ JSON map error (op={}, requestId={}, login={}, sessionId={}): merged={}",
                        op, safe(requestId), ctxLogin, ctxSessionId, shorten(merged.toString(), 1200), mapErr);

                Net_Exception_Response err = NetExceptionResponseFactory.error(
                        op,
                        requestId,
                        WireCodes.Status.BAD_REQUEST,
                        "BAD_REQUEST_FORMAT",
                        "Некорректный формат запроса: не удалось распарсить поля payload"
                );

                String out = writeResponse(err);
                if (log.isDebugEnabled()) {
                    log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
                            ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
                }
                return out;
            }

            // DEBUG: нормализованный запрос (уже распарсен)
            if (log.isDebugEnabled()) {
                log.debug("REQ OBJ (login={}, sessionId={}, op={}, requestId={}): {}",
                        ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(safeToString(request), 1200));
            }

            // 5) Вызываем хэндлер
            Net_Response response;
            try {
                response = handler.handle(request, ctx);
            } catch (Exception handlerError) {
                // ✅ Вот тут как раз и должны “появляться ошибки в логере”
                log.error("💥 Handler error (op={}, requestId={}, login={}, sessionId={})",
                        op, safe(requestId), ctxLogin, ctxSessionId, handlerError);

                Net_Exception_Response err = NetExceptionResponseFactory.error(
                        op,
                        requestId,
                        WireCodes.Status.INTERNAL_ERROR,
                        "INTERNAL_HANDLER_ERROR",
                        "Неожиданная ошибка при обработке операции: " + op
                );

                String out = writeResponse(err);
                if (log.isDebugEnabled()) {
                    log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
                            ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
                }
                return out;
            }

            // На всякий случай: если хэндлер не выставил op/requestId
            if (response.getOp() == null) response.setOp(op);
            if (response.getRequestId() == null) response.setRequestId(requestId);

            // 6) Универсальная сборка ответа
            String out = writeResponse(response);

            // DEBUG: ответ ушёл
            if (log.isDebugEnabled()) {
                log.debug("RESP OBJ (login={}, sessionId={}, op={}, requestId={}, status={}): {}",
                        ctxLogin, ctxSessionId, safe(op), safe(requestId), response.getStatus(), shorten(safeToString(response), 1200));
                log.debug("JSON OUT  (login={}, sessionId={}, op={}, requestId={}, status={}): {}",
                        ctxLogin, ctxSessionId, safe(op), safe(requestId), response.getStatus(), shorten(out, 1200));
            }

            return out;

        } catch (Exception e) {
            // ✅ Любая неожиданная ошибка парсинга/обработки — в лог
            log.error("❌ JSON processing error (op={}, requestId={}, login={}, sessionId={})",
                    safe(op), safe(requestId), safe(ctxLogin), safe(ctxSessionId), e);

            Net_Exception_Response err = NetExceptionResponseFactory.error(
                    op != null ? op : "Unknown",
                    requestId,
                    WireCodes.Status.INTERNAL_ERROR,
                    "INTERNAL_ERROR",
                    "Внутренняя ошибка сервера"
            );

            String out = writeResponse(err);

            if (log.isDebugEnabled()) {
                log.debug("JSON OUT (login={}, sessionId={}, op={}, requestId={}): {}",
                        ctxLogin, ctxSessionId, safe(op), safe(requestId), shorten(out, 1200));
            }

            return out;
        }
    }

    // --- helpers ---

    private static String getTextOrNull(JsonNode node, String field) {
        if (node == null || !node.has(field) || node.get(field).isNull()) return null;
        return node.get(field).asText();
    }

    /**
     * Унифицированная сериализация любого NetResponse в формат:
     * {
     *   "op": ...,
     *   "requestId": ...,
     *   "status": ...,
     *   "payload": { ... }
     * }
     */
    private static String writeResponse(Net_Response response) {
        try {
            // Конвертируем полный объект ответа в ObjectNode
            ObjectNode full = JSON_MAPPER.convertValue(response, ObjectNode.class);

            // То, что должно остаться наверху:
            String op = full.hasNonNull("op") ? full.get("op").asText() : null;
            String requestId = full.hasNonNull("requestId") ? full.get("requestId").asText() : null;
            int status = full.hasNonNull("status") ? full.get("status").asInt() : 0;

            // Удаляем базовые поля и payload из "полного" объекта,
            // всё остальное отправляем внутрь payload.
            full.remove("op");
            full.remove("requestId");
            full.remove("status");
            full.remove("payload");

            ObjectNode root = JSON_MAPPER.createObjectNode();
            if (op != null) root.put("op", op); else root.putNull("op");
            if (requestId != null) root.put("requestId", requestId); else root.putNull("requestId");
            root.put("status", status);

            // payload — это всё, что осталось от full (может быть пустым объектом {})
            root.set("payload", full);

            return JSON_MAPPER.writeValueAsString(root);

        } catch (Exception e) {
            // Совсем аварийный случай — сериализация ответа сломалась.
            log.error("❌ Response serialization error (op={}, requestId={})",
                    safe(response != null ? response.getOp() : null),
                    safe(response != null ? response.getRequestId() : null),
                    e);

            return "{\"op\":\"" + safe(response != null ? response.getOp() : null) +
                    "\",\"requestId\":\"" + safe(response != null ? response.getRequestId() : null) +
                    "\",\"status\":" + (response != null ? response.getStatus() : 500) +
                    ",\"payload\":{\"code\":\"SERIALIZATION_ERROR\",\"message\":\"Ошибка сериализации ответа\"}}";
        }
    }

    private static String safe(String s) {
        return s != null ? s : "";
    }

    private static String shorten(String s, int max) {
        if (s == null) return "";
        if (s.length() <= max) return s;
        return s.substring(0, Math.max(0, max)) + "...(+" + (s.length() - max) + " chars)";
    }

    private static String safeToString(Object o) {
        if (o == null) return "null";
        try {
            // Чтобы не плодить огромные логи и не утыкаться в циклические ссылки —
            // логируем как JSON, если возможно.
            return JSON_MAPPER.writeValueAsString(o);
        } catch (Exception ignore) {
            return String.valueOf(o);
        }
    }
}
////package server.logic.ws_protocol.JSON.utils;
//
//import shine.db.entities.SolanaUserEntry;
//import utils.crypto.Ed25519Util;
//
//import java.nio.charset.StandardCharsets;
//import java.util.Base64;
//
//public final class AuthSignatures {
//
//    private AuthSignatures() {}
//
//    /** preimage для CreateAuthSession(v2): "AUTH_CREATE_SESSION:login:timeMs:authNonce" */
//    public static byte[] preimageCreateAuthSession(String login, long timeMs, String authNonce) {
//        String preimageStr = "AUTH_CREATE_SESSION:" + login + ":" + timeMs + ":" + authNonce;
//        return preimageStr.getBytes(StandardCharsets.UTF_8);
//    }
//
//    /** Декод base64 / base64url (если надо — подстрой под твой decodeBase64Any) */
//    public static byte[] decodeBase64Any(String s) throws IllegalArgumentException {
//        if (s == null) throw new IllegalArgumentException("base64 is null");
//        String x = s.trim();
//        if (x.isEmpty()) throw new IllegalArgumentException("base64 is empty");
//
//        try {
//            return Base64.getDecoder().decode(x);
//        } catch (IllegalArgumentException e1) {
//            // пробуем base64url без паддинга
//            return Base64.getUrlDecoder().decode(x);
//        }
//    }
//
//    /**
//     * Проверка подписи CreateAuthSession(v2) по deviceKey пользователя.
//     * Подпись проверяется над preimageCreateAuthSession(...).
//     */
//    public static boolean verifyCreateAuthSessionSignature(
//            SolanaUserEntry user,
//            String login,
//            String authNonce,
//            long timeMs,
//            String signatureB64
//    ) throws IllegalArgumentException {
//
//        // user.getDeviceKey() — base64 публичного ключа (32 байта)
//        byte[] publicKey32 = decodeBase64Any(user.getDeviceKey());
//        byte[] signature64 = decodeBase64Any(signatureB64);
//
//        byte[] preimage = preimageCreateAuthSession(login, timeMs, authNonce);
//        return Ed25519Util.verify(preimage, signature64, publicKey32);
//    }
//}
package server.logic.ws_protocol.JSON.utils;

import server.logic.ws_protocol.JSON.entyties.Net_Exception_Response;
import server.logic.ws_protocol.JSON.entyties.Net_Request;

/**
 * Фабрика ошибок для JSON-протокола.
 * Создаёт единообразные NetExceptionResponse.
 */
public final class NetExceptionResponseFactory {

    private NetExceptionResponseFactory() {
        // запрет на создание объектов
    }

    public static Net_Exception_Response error(Net_Request req,
                                               int status,
                                               String code,
                                               String message) {

        Net_Exception_Response resp = new Net_Exception_Response();

        // ✅ НЕ падаем, даже если req == null
        if (req != null) {
            resp.setOp(req.getOp());
            resp.setRequestId(req.getRequestId());
        } else {
            resp.setOp(null);
            resp.setRequestId(null);
        }

        resp.setStatus(status);
        resp.setCode(code);
        resp.setMessage(message);
        return resp;
    }

    /**
     * Вариант для случаев, когда NetRequest ещё не распарсен,
     * но мы уже знаем op и requestId (или они null).
     */
    public static Net_Exception_Response error(String op,
                                               String requestId,
                                               int status,
                                               String code,
                                               String message) {

        Net_Exception_Response resp = new Net_Exception_Response();
        resp.setOp(op);
        resp.setRequestId(requestId);
        resp.setStatus(status);
        resp.setCode(code);
        resp.setMessage(message);
        return resp;
    }
}
