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

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import shine.db.dao.SolanaUsersDAO;
import shine.db.dao.UserCreateDAO;
import shine.db.entities.SolanaUserEntry;
import utils.config.SolanaProgramsConfig;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Locale;

/**
 * Lazy-import пользователя из Solana PDA в локальную БД сервера.
 * Используется при входе, если в solana_users нет записи по login.
 */
public final class SolanaUserPdaImportService {

    private static final Logger log = LoggerFactory.getLogger(SolanaUserPdaImportService.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final HttpClient HTTP = HttpClient.newHttpClient();
    private static final String MAGIC = "SHiNE";

    private SolanaUserPdaImportService() {}

    public static SolanaUserEntry findOrImportByLogin(String loginRaw) throws Exception {
        String login = normalizeLogin(loginRaw);
        if (login == null) return null;

        SolanaUsersDAO usersDao = SolanaUsersDAO.getInstance();
        SolanaUserEntry existing = usersDao.getByLogin(login);
        if (existing != null) return existing;

        ParsedSolanaUser parsed = fetchFromSolana(login);
        if (parsed == null) return null;

        long now = System.currentTimeMillis();
        long sizeLimit = parsed.paidLimitBytes > 0 ? parsed.paidLimitBytes : 100_000L;
        boolean inserted = UserCreateDAO.getInstance().insertUserWithBlockchain(
                parsed.login,
                parsed.blockchainName,
                parsed.deviceKeyB64,     // в текущей модели solanaKey = deviceKey
                parsed.blockchainKeyB64,
                parsed.deviceKeyB64,
                sizeLimit,
                now
        );
        if (!inserted) {
            return usersDao.getByLogin(login);
        }
        return usersDao.getByLogin(login);
    }

    public static SessionTypeCheckResult checkSessionTypeAgainstPda(String loginRaw, String sessionKeyApi, int requestedSessionType) throws Exception {
        String login = normalizeLogin(loginRaw);
        if (login == null) return SessionTypeCheckResult.noRecord();

        ParsedSolanaUser parsed = fetchFromSolana(login);
        if (parsed == null) return SessionTypeCheckResult.noRecord();

        byte[] sessionPubKey32 = AuthSessionTypeSupport.tryParseSessionPublicKey32(sessionKeyApi);
        if (sessionPubKey32 == null) return SessionTypeCheckResult.noRecord();

        for (ParsedSessionRecord session : parsed.sessions()) {
            if (constantTimeEquals(session.sessionPubKey32(), sessionPubKey32)) {
                if (session.sessionType() == requestedSessionType) {
                    return SessionTypeCheckResult.match(session.sessionType(), session.sessionName());
                }
                return SessionTypeCheckResult.mismatch(session.sessionType(), session.sessionName());
            }
        }

        return SessionTypeCheckResult.noRecord();
    }

    private static ParsedSolanaUser fetchFromSolana(String login) throws Exception {
        String loginB58 = toBase58(login.getBytes(StandardCharsets.UTF_8));
        String lenB58 = toBase58(new byte[]{(byte) login.length()});

        String reqJson = """
                {
                  "jsonrpc":"2.0",
                  "id":1,
                  "method":"getProgramAccounts",
                  "params":[
                    "%s",
                    {
                      "encoding":"base64",
                      "filters":[
                        {"memcmp":{"offset":61,"bytes":"%s"}},
                        {"memcmp":{"offset":62,"bytes":"%s"}}
                      ]
                    }
                  ]
                }
                """.formatted(SolanaProgramsConfig.SHINE_USERS_PROGRAM_ID, lenB58, loginB58);

        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(SolanaProgramsConfig.SOLANA_RPC_URL))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(reqJson))
                .build();

        HttpResponse<String> resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        if (resp.statusCode() < 200 || resp.statusCode() >= 300) {
            throw new IllegalStateException("Solana RPC HTTP status=" + resp.statusCode());
        }

        JsonNode root = MAPPER.readTree(resp.body());
        if (root.hasNonNull("error")) {
            throw new IllegalStateException("Solana RPC error: " + root.get("error").toString());
        }

        JsonNode result = root.path("result");
        if (!result.isArray() || result.isEmpty()) return null;

        for (JsonNode item : result) {
            JsonNode dataNode = item.path("account").path("data");
            if (!dataNode.isArray() || dataNode.size() < 1) continue;
            String b64 = dataNode.get(0).asText("");
            if (b64.isBlank()) continue;
            byte[] raw = Base64.getDecoder().decode(b64);
            ParsedSolanaUser parsed = parseUserPda(raw);
            if (parsed != null && parsed.login.equalsIgnoreCase(login)) {
                return parsed;
            }
        }
        return null;
    }

    private static ParsedSolanaUser parseUserPda(byte[] raw) {
        if (raw == null || raw.length < 128) return null;
        if (!MAGIC.equals(new String(raw, 0, 5, StandardCharsets.UTF_8))) return null;

        int recordLen = u16le(raw, 7);
        if (recordLen < 73 || recordLen > raw.length) return null;

        int c = 9;
        c += 8;  // created_at_ms
        c += 8;  // updated_at_ms
        c += 4;  // record_number
        c += 32; // prev_record_hash

        int loginLen = u8(raw, c++);
        if (loginLen <= 0 || c + loginLen > recordLen) return null;
        String login = new String(raw, c, loginLen, StandardCharsets.UTF_8);
        c += loginLen;

        int blocksCount = u8(raw, c++);
        String blockchainName = null;
        byte[] blockchainKey32 = null;
        byte[] deviceKey32 = null;
        long paidLimitBytes = 0L;
        List<ParsedSessionRecord> sessions = new ArrayList<>();

        for (int i = 0; i < blocksCount; i++) {
            int blockType = u8(raw, c++);
            int blockVer = u8(raw, c++);
            if (blockVer != 0) return null;

            if (blockType == 1) {
                c += 32;
            } else if (blockType == 2) {
                deviceKey32 = slice(raw, c, 32);
                c += 32;
            } else if (blockType == 3) {
                int count = u8(raw, c++);
                for (int j = 0; j < count; j++) {
                    c += 1; // blockchain_type
                    int bchLen = u8(raw, c++);
                    blockchainName = new String(raw, c, bchLen, StandardCharsets.UTF_8);
                    c += bchLen;
                    blockchainKey32 = slice(raw, c, 32);
                    c += 32;
                    paidLimitBytes = u64le(raw, c);
                    c += 8;
                    c += 8;  // used_bytes
                    c += 4;  // last_block_number
                    c += 32; // last_block_hash
                    c += 64; // last_block_signature
                    int arweavePresent = u8(raw, c++);
                    if (arweavePresent == 1) {
                        int arLen = u8(raw, c++);
                        c += arLen;
                    } else if (arweavePresent != 0) {
                        return null;
                    }
                }
            } else if (blockType == 30) {
                int isServer = u8(raw, c++);
                if (isServer == 1) {
                    c += 1; // address_format_type
                    c += 1; // address_format_version
                    int addrLen = u8(raw, c++);
                    c += addrLen;
                    int syncCount = u8(raw, c++);
                    for (int j = 0; j < syncCount; j++) {
                        int n = u8(raw, c++);
                        c += n;
                    }
                } else if (isServer != 0) {
                    return null;
                }
            } else if (blockType == 40) {
                int accessCount = u8(raw, c++);
                for (int j = 0; j < accessCount; j++) {
                    int n = u8(raw, c++);
                    c += n;
                }
            } else if (blockType == 50) {
                int sessionsMode = u8(raw, c++);
                if (sessionsMode != 1 && sessionsMode != 10) return null;
                int sessionsCount = u8(raw, c++);
                if (sessionsCount > 64) return null;
                for (int j = 0; j < sessionsCount; j++) {
                    int sessionType = u8(raw, c++);
                    int sessionVersion = u8(raw, c++);
                    int n = u8(raw, c++);
                    String sessionName = new String(raw, c, n, StandardCharsets.UTF_8);
                    c += n;
                    byte[] sessionPubKey32 = slice(raw, c, 32);
                    c += 32;
                    sessions.add(new ParsedSessionRecord(
                            sessionType,
                            sessionVersion,
                            sessionName,
                            sessionPubKey32
                    ));
                }
            } else if (blockType == 70) {
                c += 1;
            } else {
                return null;
            }

            if (c > recordLen) return null;
        }

        if (blockchainName == null || blockchainKey32 == null || deviceKey32 == null) return null;
        return new ParsedSolanaUser(
                login,
                blockchainName,
                Base64.getEncoder().encodeToString(blockchainKey32),
                Base64.getEncoder().encodeToString(deviceKey32),
                paidLimitBytes,
                sessions
        );
    }

    private static String normalizeLogin(String login) {
        if (login == null) return null;
        String s = login.trim();
        if (s.isEmpty()) return null;
        return s.toLowerCase(Locale.ROOT);
    }

    private static int u8(byte[] b, int o) { return b[o] & 0xFF; }
    private static int u16le(byte[] b, int o) {
        return (b[o] & 0xFF) | ((b[o + 1] & 0xFF) << 8);
    }
    private static long u64le(byte[] b, int o) {
        long out = 0L;
        for (int i = 0; i < 8; i++) out |= ((long) (b[o + i] & 0xFF)) << (8 * i);
        return out;
    }
    private static byte[] slice(byte[] b, int o, int len) {
        byte[] out = new byte[len];
        System.arraycopy(b, o, out, 0, len);
        return out;
    }

    // Нужен для memcmp.bytes в Solana RPC.
    private static String toBase58(byte[] input) {
        final char[] alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
        int zeros = 0;
        while (zeros < input.length && input[zeros] == 0) zeros++;
        byte[] tmp = new byte[input.length * 2];
        int j = tmp.length;
        int startAt = zeros;
        while (startAt < input.length) {
            int mod = divmod58(input, startAt);
            if (input[startAt] == 0) startAt++;
            tmp[--j] = (byte) alphabet[mod];
        }
        while (j < tmp.length && tmp[j] == alphabet[0]) j++;
        while (--zeros >= 0) tmp[--j] = (byte) alphabet[0];
        return new String(tmp, j, tmp.length - j, StandardCharsets.US_ASCII);
    }

    private static int divmod58(byte[] number, int startAt) {
        int remainder = 0;
        for (int i = startAt; i < number.length; i++) {
            int digit256 = number[i] & 0xFF;
            int temp = remainder * 256 + digit256;
            number[i] = (byte) (temp / 58);
            remainder = temp % 58;
        }
        return remainder;
    }

    private static boolean constantTimeEquals(byte[] a, byte[] b) {
        if (a == null || b == null || a.length != b.length) return false;
        int diff = 0;
        for (int i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
        return diff == 0;
    }

    private record ParsedSolanaUser(
            String login,
            String blockchainName,
            String blockchainKeyB64,
            String deviceKeyB64,
            long paidLimitBytes,
            List<ParsedSessionRecord> sessions
    ) {}

    private record ParsedSessionRecord(
            int sessionType,
            int sessionVersion,
            String sessionName,
            byte[] sessionPubKey32
    ) {}

    public record SessionTypeCheckResult(
            boolean hasPdaSessionRecord,
            boolean matchesRequestedType,
            int pdaSessionType,
            String sessionName
    ) {
        static SessionTypeCheckResult noRecord() {
            return new SessionTypeCheckResult(false, true, 0, "");
        }

        static SessionTypeCheckResult match(int pdaSessionType, String sessionName) {
            return new SessionTypeCheckResult(true, true, pdaSessionType, sessionName == null ? "" : sessionName);
        }

        static SessionTypeCheckResult mismatch(int pdaSessionType, String sessionName) {
            return new SessionTypeCheckResult(true, false, pdaSessionType, sessionName == null ? "" : sessionName);
        }
    }
}
