package server.logic.ws_protocol.JSON.messages;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

final class SignedMessageBlock {
    static final byte[] LEGACY_PREFIX = "SHiNE_dm2".getBytes(StandardCharsets.US_ASCII);
    static final byte[] V1_PREFIX = "SHiNE_DM".getBytes(StandardCharsets.US_ASCII);
    static final int TYPE_INCOMING_TEXT = 1;
    static final int TYPE_OUTGOING_COPY = 2;
    static final int TYPE_READ_INCOMING = 3;
    static final int TYPE_READ_OUTGOING_COPY = 4;

    final String toLogin;
    final String fromLogin;
    final long timeMs;
    final long nonce;
    final int messageType;
    final long revisionTimeMs;
    final int formatVersionMajor;
    final int formatVersionMinor;
    final byte[] payloadBytes;
    final byte[] encryptedBodyBytes;
    final byte[] signedBody;
    final byte[] signature64;
    final byte[] rawPacket;
    final boolean legacyFormat;

    private SignedMessageBlock(
            String toLogin,
            String fromLogin,
            long timeMs,
            long nonce,
            int messageType,
            long revisionTimeMs,
            int formatVersionMajor,
            int formatVersionMinor,
            byte[] payloadBytes,
            byte[] encryptedBodyBytes,
            byte[] signedBody,
            byte[] signature64,
            byte[] rawPacket,
            boolean legacyFormat
    ) {
        this.toLogin = toLogin;
        this.fromLogin = fromLogin;
        this.timeMs = timeMs;
        this.nonce = nonce;
        this.messageType = messageType;
        this.revisionTimeMs = revisionTimeMs;
        this.formatVersionMajor = formatVersionMajor;
        this.formatVersionMinor = formatVersionMinor;
        this.payloadBytes = payloadBytes;
        this.encryptedBodyBytes = encryptedBodyBytes;
        this.signedBody = signedBody;
        this.signature64 = signature64;
        this.rawPacket = rawPacket;
        this.legacyFormat = legacyFormat;
    }

    static SignedMessageBlock parse(byte[] raw, int maxEncryptedBodyBytes) {
        if (raw == null || raw.length < 64) {
            throw new IllegalArgumentException("BAD_LEN");
        }

        if (startsWith(raw, LEGACY_PREFIX)) {
            return parseLegacy(raw, maxEncryptedBodyBytes);
        }
        if (startsWith(raw, V1_PREFIX)) {
            return parseV1(raw, maxEncryptedBodyBytes);
        }
        throw new IllegalArgumentException("BAD_PREFIX");
    }

    private static SignedMessageBlock parseLegacy(byte[] raw, int maxPayloadBytes) {
        if (raw.length < LEGACY_PREFIX.length + 1 + 1 + 8 + 4 + 2 + 2 + 64) {
            throw new IllegalArgumentException("BAD_LEN");
        }
        ByteBuffer bb = ByteBuffer.wrap(raw).order(ByteOrder.BIG_ENDIAN);
        bb.position(LEGACY_PREFIX.length);

        String toLogin = readAscii(bb, 1, 60, "BAD_TO_LOGIN");
        String fromLogin = readAscii(bb, 1, 60, "BAD_FROM_LOGIN");

        long timeMs = bb.getLong();
        if (timeMs < 0) throw new IllegalArgumentException("BAD_TIME");

        long nonce = Integer.toUnsignedLong(bb.getInt());
        int messageType = Short.toUnsignedInt(bb.getShort());
        ensureMessageType(messageType);

        int payloadLen = Short.toUnsignedInt(bb.getShort());
        if (payloadLen < 1 || payloadLen > maxPayloadBytes) {
            throw new IllegalArgumentException("BAD_MESSAGE_LEN");
        }
        if (bb.remaining() != payloadLen + 64) {
            throw new IllegalArgumentException("BAD_LEN");
        }

        byte[] payload = new byte[payloadLen];
        bb.get(payload);
        byte[] signature64 = new byte[64];
        bb.get(signature64);
        byte[] signedBody = Arrays.copyOf(raw, raw.length - 64);

        return new SignedMessageBlock(
                toLogin,
                fromLogin,
                timeMs,
                nonce,
                messageType,
                0L,
                2,
                0,
                payload,
                payload,
                signedBody,
                signature64,
                raw,
                true
        );
    }

    private static SignedMessageBlock parseV1(byte[] raw, int maxEncryptedBodyBytes) {
        if (raw.length < V1_PREFIX.length + 2 + 1 + 1 + 8 + 4 + 2 + 8 + 1 + 4 + 64) {
            throw new IllegalArgumentException("BAD_LEN");
        }
        ByteBuffer bb = ByteBuffer.wrap(raw).order(ByteOrder.BIG_ENDIAN);
        bb.position(V1_PREFIX.length);

        int major = Byte.toUnsignedInt(bb.get());
        int minor = Byte.toUnsignedInt(bb.get());
        if (major != 1 || minor != 0) {
            throw new IllegalArgumentException("BAD_FORMAT_VERSION");
        }

        String toLogin = readAscii(bb, 1, 60, "BAD_TO_LOGIN");
        String fromLogin = readAscii(bb, 1, 60, "BAD_FROM_LOGIN");
        long timeMs = bb.getLong();
        if (timeMs < 0) throw new IllegalArgumentException("BAD_TIME");
        long nonce = Integer.toUnsignedLong(bb.getInt());
        int messageType = Short.toUnsignedInt(bb.getShort());
        ensureMessageType(messageType);
        long revisionTimeMs = bb.getLong();
        if (revisionTimeMs < 0) throw new IllegalArgumentException("BAD_REVISION_TIME");

        int attachmentsCount = Byte.toUnsignedInt(bb.get());
        if (attachmentsCount != 0) {
            throw new IllegalArgumentException("ATTACHMENTS_DISABLED");
        }

        if (bb.remaining() < 4 + 64) {
            throw new IllegalArgumentException("BAD_LEN");
        }
        long encryptedBodyLen = Integer.toUnsignedLong(bb.getInt());
        if (encryptedBodyLen > maxEncryptedBodyBytes) {
            throw new IllegalArgumentException("BAD_MESSAGE_LEN");
        }
        if (bb.remaining() != encryptedBodyLen + 64) {
            throw new IllegalArgumentException("BAD_LEN");
        }
        byte[] encryptedBody = new byte[(int) encryptedBodyLen];
        bb.get(encryptedBody);
        byte[] signature64 = new byte[64];
        bb.get(signature64);
        byte[] signedBody = Arrays.copyOf(raw, raw.length - 64);

        return new SignedMessageBlock(
                toLogin,
                fromLogin,
                timeMs,
                nonce,
                messageType,
                revisionTimeMs,
                major,
                minor,
                encryptedBody,
                encryptedBody,
                signedBody,
                signature64,
                raw,
                false
        );
    }

    boolean isIncomingType() {
        return messageType == TYPE_INCOMING_TEXT || messageType == TYPE_READ_INCOMING;
    }

    boolean isOutgoingCopyType() {
        return messageType == TYPE_OUTGOING_COPY || messageType == TYPE_READ_OUTGOING_COPY;
    }

    boolean isContentType() {
        return messageType == TYPE_INCOMING_TEXT || messageType == TYPE_OUTGOING_COPY;
    }

    boolean isReadReceiptType() {
        return messageType == TYPE_READ_INCOMING || messageType == TYPE_READ_OUTGOING_COPY;
    }

    boolean isDeletedContent() {
        return isContentType() && !legacyFormat && encryptedBodyBytes.length == 0;
    }

    String targetLogin() {
        return isIncomingType() ? toLogin : fromLogin;
    }

    private static void ensureMessageType(int messageType) {
        if (messageType < TYPE_INCOMING_TEXT || messageType > TYPE_READ_OUTGOING_COPY) {
            throw new IllegalArgumentException("BAD_MESSAGE_TYPE");
        }
    }

    private static boolean startsWith(byte[] raw, byte[] prefix) {
        if (raw.length < prefix.length) return false;
        for (int i = 0; i < prefix.length; i++) {
            if (raw[i] != prefix[i]) return false;
        }
        return true;
    }

    private static String readAscii(ByteBuffer bb, int minLen, int maxLen, String code) {
        if (!bb.hasRemaining()) throw new IllegalArgumentException(code);
        int len = Byte.toUnsignedInt(bb.get());
        if (len < minLen || len > maxLen || bb.remaining() < len) {
            throw new IllegalArgumentException(code);
        }
        byte[] bytes = new byte[len];
        bb.get(bytes);
        for (byte b : bytes) {
            if (b < 0x20 || b > 0x7E) throw new IllegalArgumentException(code);
        }
        return new String(bytes, StandardCharsets.US_ASCII);
    }
}
