package test.it.cases;

import blockchain.MsgSubType;
import blockchain.body.ConnectionBody;
import blockchain.body.HeaderBody;
import blockchain.body.TextBody;
import blockchain.body.UserParamBody;
import test.it.blockchain.AddBlockSender;
import test.it.blockchain.ChainState;
import test.it.utils.TestIds;
import test.it.utils.json.JsonParsers;
import test.it.utils.log.TestResult;
import test.it.utils.ws.WsSession;
import utils.crypto.Ed25519Util;
import utils.crypto.HashSHA256Util;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * Вспомогательные функции для массового заполнения тестовой соц-сети через API.
 * Вся запись состояния делается только через AddUser / AddBlock.
 */
public final class SeedDataPopulationHelper {

    private static final String BCH_SUFFIX = "001";

    private final WsSession ws;
    private final TestResult result;
    private final Duration timeout;
    private final UserKeys keys;

    private final Map<String, ChainState> states = new LinkedHashMap<>();
    private final Map<String, AddBlockSender> senders = new LinkedHashMap<>();
    private final Map<String, byte[]> headerHashes = new LinkedHashMap<>();

    public SeedDataPopulationHelper(WsSession ws, TestResult result, Duration timeout, String password) {
        this.ws = Objects.requireNonNull(ws, "ws");
        this.result = Objects.requireNonNull(result, "result");
        this.timeout = Objects.requireNonNull(timeout, "timeout");
        this.keys = deriveKeysFromPassword(String.valueOf(password == null ? "" : password));
    }

    public static UserSpec user(String login, String firstName, String lastName) {
        return new UserSpec(login, firstName, lastName);
    }

    public void createUsersAndHeaders(List<UserSpec> users) {
        for (UserSpec user : users) createUserViaApi(user.login);
        for (UserSpec user : users) initHeader(user.login);
    }

    public void applyProfiles(List<UserSpec> users) {
        for (UserSpec user : users) {
            upsertProfileParam(user.login, "first_name", user.firstName);
            upsertProfileParam(user.login, "last_name", user.lastName);
            upsertProfileParam(user.login, "gender", user.gender);
            if (notBlank(user.address)) upsertProfileParam(user.login, "address", user.address);
            if (notBlank(user.web)) upsertProfileParam(user.login, "web", user.web);
            if (notBlank(user.phone)) upsertProfileParam(user.login, "phone", user.phone);
            if (user.official != null) upsertProfileParam(user.login, "official", user.official ? "yes" : "no");
            if (user.shine != null) upsertProfileParam(user.login, "shine", user.shine ? "yes" : "no");
        }
    }

    public void addCloseFriend(String from, String to) {
        sendConnection(from, to, MsgSubType.CONNECTION_CLOSE_FRIEND);
    }

    public void addMutualCloseFriend(String a, String b) {
        addCloseFriend(a, b);
        addCloseFriend(b, a);
    }

    public void addContact(String from, String to) {
        sendConnection(from, to, MsgSubType.CONNECTION_CONTACT);
    }

    public void addMutualContact(String a, String b) {
        addContact(a, b);
        addContact(b, a);
    }

    public void addFollow(String from, String to) {
        sendConnection(from, to, MsgSubType.CONNECTION_FOLLOW);
    }

    public void addParent(String whoAddsParent, String parentLogin) {
        sendConnection(whoAddsParent, parentLogin, MsgSubType.CONNECTION_PARENT);
    }

    public void addChild(String whoAddsChild, String childLogin) {
        sendConnection(whoAddsChild, childLogin, MsgSubType.CONNECTION_CHILD);
    }

    public void addSibling(String from, String to) {
        sendConnection(from, to, MsgSubType.CONNECTION_SIBLING);
    }

    public void addMutualSibling(String a, String b) {
        addSibling(a, b);
        addSibling(b, a);
    }

    public void addPost(String login, String text) {
        String cleanText = String.valueOf(text == null ? "" : text).trim();
        if (cleanText.isEmpty()) return;
        AddBlockSender sender = senderFor(login);
        ChainState state = stateFor(login);
        ChainState.NextLine line = state.nextTextLineByCode(0);
        sender.send(TextBody.newPost(
                line.lineCode,
                line.prevLineNumber,
                line.prevLineHash32,
                line.thisLineNumber,
                cleanText
        ), timeout);
    }

    private void upsertProfileParam(String login, String key, String value) {
        String cleanKey = String.valueOf(key == null ? "" : key).trim();
        String cleanValue = String.valueOf(value == null ? "" : value).trim();
        if (cleanKey.isEmpty() || cleanValue.isEmpty()) return;

        AddBlockSender sender = senderFor(login);
        ChainState state = stateFor(login);
        ChainState.NextLine line = state.nextLineByType(ChainState.TYPE_USER_PARAM);
        sender.send(new UserParamBody(
                0,
                line.prevLineNumber,
                line.prevLineHash32,
                line.thisLineNumber,
                cleanKey,
                cleanValue
        ), timeout);
    }

    private void sendConnection(String from, String to, short relationSubType) {
        AddBlockSender sender = senderFor(from);
        ChainState state = stateFor(from);
        byte[] targetHeaderHash = headerHashFor(to);
        ChainState.NextLine line = state.nextLineByType(ChainState.TYPE_CONNECTION);

        sender.send(new ConnectionBody(
                0,
                line.prevLineNumber,
                line.prevLineHash32,
                line.thisLineNumber,
                relationSubType,
                bch(to),
                0,
                targetHeaderHash
        ), timeout);
    }

    private void createUserViaApi(String login) {
        String requestId = TestIds.next("seed_adduser");
        String req = """
                {
                  "op": "AddUser",
                  "requestId": "%s",
                  "payload": {
                    "login": "%s",
                    "blockchainName": "%s",
                    "solanaKey": "%s",
                    "blockchainKey": "%s",
                    "deviceKey": "%s",
                    "bchLimit": 50000000
                  }
                }
                """.formatted(
                requestId,
                login,
                bch(login),
                keys.solanaPublicB64,
                keys.blockchainPublicB64,
                keys.devicePublicB64
        );

        String resp = ws.call("AddUser#" + login, req, timeout);
        int status = JsonParsers.status(resp);
        if (status == 200) {
            result.ok("AddUser " + login + ": created");
            return;
        }
        if (status == 409) {
            String code = JsonParsers.errorCode(resp);
            if ("USER_ALREADY_EXISTS".equals(code)
                    || "BLOCKCHAIN_ALREADY_EXISTS".equals(code)
                    || "BLOCKCHAIN_STATE_ALREADY_EXISTS".equals(code)) {
                result.ok("AddUser " + login + ": already exists (" + code + ")");
                return;
            }
        }
        throw new IllegalStateException("AddUser failed for " + login + ": status=" + status + ", resp=" + resp);
    }

    private void initHeader(String login) {
        ChainState state = new ChainState();
        AddBlockSender sender = new AddBlockSender(ws, state, login, bch(login), keys.blockchainPrivate32);
        sender.send(new HeaderBody(login), timeout);

        byte[] headerHash = state.getHash32(0);
        if (headerHash == null || headerHash.length != 32) {
            throw new IllegalStateException("Header hash is missing for " + login);
        }

        states.put(login, state);
        senders.put(login, sender);
        headerHashes.put(login, headerHash);
        result.ok("HEADER создан: " + login);
    }

    private AddBlockSender senderFor(String login) {
        AddBlockSender sender = senders.get(login);
        if (sender == null) throw new IllegalStateException("Sender not initialized for login=" + login);
        return sender;
    }

    private ChainState stateFor(String login) {
        ChainState state = states.get(login);
        if (state == null) throw new IllegalStateException("State not initialized for login=" + login);
        return state;
    }

    private byte[] headerHashFor(String login) {
        byte[] hash = headerHashes.get(login);
        if (hash == null) throw new IllegalStateException("Header hash not initialized for login=" + login);
        return hash;
    }

    private static String bch(String login) {
        return login + "-" + BCH_SUFFIX;
    }

    private static boolean notBlank(String value) {
        return value != null && !value.trim().isEmpty();
    }

    private static UserKeys deriveKeysFromPassword(String password) {
        byte[] base = HashSHA256Util.sha256(password.getBytes(StandardCharsets.UTF_8));
        String baseB64 = Base64.getEncoder().encodeToString(base);

        byte[] rootPriv = HashSHA256Util.sha256((baseB64 + "root.key").getBytes(StandardCharsets.UTF_8));
        byte[] bchPriv = HashSHA256Util.sha256((baseB64 + "bch.key").getBytes(StandardCharsets.UTF_8));
        byte[] devPriv = HashSHA256Util.sha256((baseB64 + "dev.key").getBytes(StandardCharsets.UTF_8));

        String rootPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(rootPriv));
        String bchPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(bchPriv));
        String devPubB64 = Base64.getEncoder().encodeToString(Ed25519Util.derivePublicKey(devPriv));

        return new UserKeys(rootPubB64, bchPubB64, devPubB64, bchPriv);
    }

    public static final class UserSpec {
        public final String login;
        public final String firstName;
        public final String lastName;

        public String gender = "unknown";
        public String address = "";
        public String web = "";
        public String phone = "";
        public Boolean official = null;
        public Boolean shine = null;

        private UserSpec(String login, String firstName, String lastName) {
            this.login = String.valueOf(login == null ? "" : login).trim();
            this.firstName = String.valueOf(firstName == null ? "" : firstName).trim();
            this.lastName = String.valueOf(lastName == null ? "" : lastName).trim();
            if (this.login.isEmpty()) throw new IllegalArgumentException("login is empty");
            if (this.firstName.isEmpty()) throw new IllegalArgumentException("firstName is empty for " + this.login);
            if (this.lastName.isEmpty()) throw new IllegalArgumentException("lastName is empty for " + this.login);
        }

        public UserSpec male() { this.gender = "male"; return this; }
        public UserSpec female() { this.gender = "female"; return this; }
        public UserSpec unknownGender() { this.gender = "unknown"; return this; }
        public UserSpec address(String value) { this.address = String.valueOf(value == null ? "" : value).trim(); return this; }
        public UserSpec web(String value) { this.web = String.valueOf(value == null ? "" : value).trim(); return this; }
        public UserSpec phone(String value) { this.phone = String.valueOf(value == null ? "" : value).trim(); return this; }
        public UserSpec official(boolean value) { this.official = value; return this; }
        public UserSpec shine(boolean value) { this.shine = value; return this; }
    }

    private static final class UserKeys {
        final String solanaPublicB64;
        final String blockchainPublicB64;
        final String devicePublicB64;
        final byte[] blockchainPrivate32;

        private UserKeys(String solanaPublicB64,
                         String blockchainPublicB64,
                         String devicePublicB64,
                         byte[] blockchainPrivate32) {
            this.solanaPublicB64 = solanaPublicB64;
            this.blockchainPublicB64 = blockchainPublicB64;
            this.devicePublicB64 = devicePublicB64;
            this.blockchainPrivate32 = blockchainPrivate32;
        }
    }
}
