package test.it.blockchain;

import blockchain.BchBlockEntry;
import blockchain.body.*;
import test.it.utils.TestConfig;
import test.it.utils.TestIds;
import test.it.utils.json.JsonParsers;
import test.it.utils.log.TestLog;
import test.it.utils.ws.WsSession;

import java.time.Duration;
import java.util.Base64;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
 * AddBlockSender — под новый формат BchBlockEntry (Frame v0):
 *  - blockBytes = preimage + sigMarker(2) + signature64
 *  - preimage начинается с frameCode(2) = 0x0000
 *  - hash32 вычисляется как sha256(preimage)
 *  - signature = Ed25519.sign(hash32)
 *
 * ВАЖНО:
 *  - Линии по ТЗ ведём на стороне сервера/БД (триггеры), а в тестах считаем локально.
 */
public final class AddBlockSender {

    private static final String ZERO64 = "0".repeat(64);

    private final WsSession ws;
    private final ChainState state;

    private final String login;
    private final String blockchainName;
    private final byte[] loginPrivKey;

    public AddBlockSender(WsSession ws, ChainState state, String login, String blockchainName, byte[] loginPrivKey) {
        this.ws = ws;
        this.state = state;
        this.login = login;
        this.blockchainName = blockchainName;
        this.loginPrivKey = (loginPrivKey == null ? null : loginPrivKey.clone());
        if (this.ws == null) throw new IllegalArgumentException("ws == null");
        if (this.state == null) throw new IllegalArgumentException("state == null");
        if (this.loginPrivKey == null) throw new IllegalArgumentException("loginPrivKey == null");
    }

    public ChainState state() { return state; }

    public void send(BodyRecord body, Duration timeout) {
        if (body == null) throw new IllegalArgumentException("body == null");

        body.check();

        boolean isHeader = (body instanceof HeaderBody);

        if (isHeader) {
            if (state.lastBlockNumber() != -1) {
                throw new IllegalStateException("HEADER должен быть первым: lastBlockNumber уже " + state.lastBlockNumber());
            }
        } else {
            if (!state.hasHeader()) {
                throw new IllegalStateException("Нельзя слать блоки до HEADER (нет headerHash32)");
            }
        }

        int blockNumber = state.nextBlockNumber();
        byte[] prevHash32 = state.prevHash32ForNext();
        long tsSec = System.currentTimeMillis() / 1000L;

        short type = typeOf(body);
        short subType = subTypeOf(body);
        short version = versionOf(body);

        byte[] bodyBytes = body.toBytes();

        // ВАЖНО: preimage должен быть БАЙТ-В-БАЙТ таким же, как в BchBlockEntry
        byte[] preimage = buildPreimage(prevHash32, blockNumber, tsSec, type, subType, version, bodyBytes);

        byte[] hash32 = blockchain.BchCryptoVerifier.sha256(preimage);
        byte[] signature64 = utils.crypto.Ed25519Util.sign(hash32, loginPrivKey);

        BchBlockEntry entry = new BchBlockEntry(
                prevHash32,
                blockNumber,
                tsSec,
                type,
                subType,
                version,
                bodyBytes,
                signature64
        );

        String prevHashHexForReq = (blockNumber == 0) ? ZERO64 : state.lastBlockHashHex();

        String reqJson = buildAddBlockJson(blockchainName, blockNumber, prevHashHexForReq, base64(entry.toBytes()));
        String op = "AddBlock(user=" + login + ", block=" + blockNumber + ", type=" + (type & 0xFFFF) + ", sub=" + (subType & 0xFFFF) + ")";

        String resp = ws.call(op, reqJson, timeout);

        assert200(op, resp);

        String serverLastHash = JsonMini.extractPayloadString(resp, "serverLastBlockHash");
        if (serverLastHash == null) {
            serverLastHash = JsonMini.extractPayloadString(resp, "serverLastGlobalHash");
        }

        assertNotNull(serverLastHash, op + ": payload.serverLastBlockHash must not be null");
        assertEquals(64, serverLastHash.trim().length(), op + ": serverLastBlockHash must be 64 hex chars");

        String localHashHex = bytesToHex64(entry.getHash32());

        if (TestConfig.DEBUG()) {
            TestLog.info(op + ": localHash=" + localHashHex);
            TestLog.info(op + ": serverLastBlockHash=" + serverLastHash);
        }

        assertEquals(localHashHex, serverLastHash, op + ": serverLastBlockHash must match local hash");

        state.applyAppendedBlock(blockNumber, entry.getHash32(), isHeader, type, body);

        if (TestConfig.DEBUG()) TestLog.info(op + ": state updated");
    }

    // ---------- request JSON ----------

    private static String buildAddBlockJson(String blockchainName, int blockNumber, String prevBlockHashHex, String blockBytesB64) {
        String requestId = TestIds.next("addblock");
        return """
            {
              "op": "AddBlock",
              "requestId": "%s",
              "payload": {
                "blockchainName": "%s",
                "blockNumber": %d,
                "prevBlockHash": "%s",
                "blockBytesB64": "%s"
              }
            }
            """.formatted(requestId, blockchainName, blockNumber, prevBlockHashHex, blockBytesB64);
    }

    private static void assert200(String op, String resp) {
        int st = JsonParsers.status(resp);
        assertEquals(200, st, op + ": expected status=200, but got=" + st + ", resp=" + resp);
        TestLog.ok(op + ": status=200");
    }

    private static String base64(byte[] bytes) {
        return Base64.getEncoder().encodeToString(bytes);
    }

    private static String bytesToHex64(byte[] b32) {
        char[] out = new char[64];
        final char[] HEX = "0123456789abcdef".toCharArray();
        for (int i = 0; i < 32; i++) {
            int v = b32[i] & 0xFF;
            out[i * 2] = HEX[v >>> 4];
            out[i * 2 + 1] = HEX[v & 0x0F];
        }
        return new String(out);
    }

    // ---------- header extraction from body ----------

    private static short typeOf(BodyRecord body) {
        if (body instanceof HeaderBody) return HeaderBody.TYPE;
        if (body instanceof CreateChannelBody) return CreateChannelBody.TYPE;
        if (body instanceof TextBody) return TextBody.TYPE;
        if (body instanceof ReactionBody) return ReactionBody.TYPE;
        if (body instanceof ConnectionBody) return ConnectionBody.TYPE;
        if (body instanceof UserParamBody) return UserParamBody.TYPE;
        throw new IllegalArgumentException("Unknown body class: " + body.getClass());
    }

    private static short subTypeOf(BodyRecord body) {
        if (body instanceof HeaderBody hb) return hb.subType;
        if (body instanceof CreateChannelBody cb) return cb.subType;
        if (body instanceof TextBody tb) return tb.subType;
        if (body instanceof ReactionBody rb) return rb.subType;
        if (body instanceof ConnectionBody cb) return cb.subType;
        if (body instanceof UserParamBody ub) return ub.subType;
        throw new IllegalArgumentException("Unknown body class: " + body.getClass());
    }

    private static short versionOf(BodyRecord body) {
        if (body instanceof HeaderBody hb) return hb.version;
        if (body instanceof CreateChannelBody cb) return cb.version;
        if (body instanceof TextBody tb) return tb.version;
        if (body instanceof ReactionBody rb) return rb.version;
        if (body instanceof ConnectionBody cb) return cb.version;
        if (body instanceof UserParamBody ub) return ub.version;
        throw new IllegalArgumentException("Unknown body class: " + body.getClass());
    }

    // ---------- preimage builder (строго по BchBlockEntry Frame v0) ----------

    private static byte[] buildPreimage(byte[] prevHash32,
                                        int blockNumber,
                                        long tsSec,
                                        short type,
                                        short subType,
                                        short version,
                                        byte[] bodyBytes) {

        if (prevHash32 == null || prevHash32.length != 32) {
            throw new IllegalArgumentException("prevHash32 must be 32 bytes");
        }

        int bodyLen = (bodyBytes == null ? 0 : bodyBytes.length);
        int blockSize = BchBlockEntry.PREIMAGE_HEADER_SIZE + bodyLen;

        java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(blockSize).order(java.nio.ByteOrder.BIG_ENDIAN);

        // [2] frameCode (v0)
        bb.putShort((short) (BchBlockEntry.FRAME_CODE_V0 & 0xFFFF));

        // [32] prevHash32
        bb.put(prevHash32);

        // [4] blockSize (preimage size)
        bb.putInt(blockSize);

        // [4] blockNumber
        bb.putInt(blockNumber);

        // [8] timestamp
        bb.putLong(tsSec);

        // [2][2][2] type/subType/version
        bb.putShort(type);
        bb.putShort(subType);
        bb.putShort(version);

        // [N] bodyBytes
        if (bodyBytes != null) bb.put(bodyBytes);

        return bb.array();
    }
}