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

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import shine.db.dao.TestFreeAvatarUploadsDAO;
import shine.db.entities.TestFreeAvatarUploadEntry;
import utils.config.AppConfig;

import java.io.IOException;
import java.math.BigInteger;
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.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.sql.SQLException;
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.concurrent.ConcurrentHashMap;

public final class TestFreeAvatarArweaveService {

    private static final Logger log = LoggerFactory.getLogger(TestFreeAvatarArweaveService.class);
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private static final HttpClient HTTP = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(20))
            .build();
    private static final Base64.Decoder B64URL = Base64.getUrlDecoder();
    private static final Base64.Encoder B64URL_NOPAD = Base64.getUrlEncoder().withoutPadding();
    private static final int DEFAULT_LIMIT = 3;
    private static final int DEFAULT_MAX_BYTES = 128 * 1024;
    private static final String DEFAULT_GATEWAY = "https://arweave.net";
    private static final ConcurrentHashMap<String, Object> LOGIN_LOCKS = new ConcurrentHashMap<>();

    private final AppConfig config = AppConfig.getInstance();
    private final TestFreeAvatarUploadsDAO quotaDao = TestFreeAvatarUploadsDAO.getInstance();

    public Quota getQuota(String login) throws SQLException {
        int limit = getLimitPerUser();
        TestFreeAvatarUploadEntry entry = quotaDao.getByLogin(login);
        int used = entry == null ? 0 : Math.max(0, entry.getUsedCount());
        int remaining = Math.max(0, limit - used);
        return new Quota(limit, used, remaining, isEnabled());
    }

    public UploadResult uploadAvatar(String login, String contentType, byte[] fileBytes, String expectedSha256Hex)
            throws Exception {
        if (!isEnabled()) {
            throw new IllegalStateException("Временная бесплатная загрузка аватаров сейчас отключена на сервере.");
        }

        String cleanLogin = String.valueOf(login == null ? "" : login).trim();
        if (cleanLogin.isBlank()) {
            throw new IllegalArgumentException("Пустой login для бесплатной загрузки аватара.");
        }

        String cleanType = normalizeContentType(contentType);
        validatePayload(cleanType, fileBytes);
        String actualSha256Hex = sha256Hex(fileBytes);
        String expectedSha = String.valueOf(expectedSha256Hex == null ? "" : expectedSha256Hex).trim().toLowerCase();
        if (!expectedSha.isBlank() && !actualSha256Hex.equals(expectedSha)) {
            throw new IllegalArgumentException("SHA-256 файла не совпадает с присланным клиентом.");
        }

        Object lock = LOGIN_LOCKS.computeIfAbsent(cleanLogin.toLowerCase(), key -> new Object());
        synchronized (lock) {
            Quota before = getQuota(cleanLogin);
            if (before.remainingCount() <= 0) {
                throw new FreeAvatarLimitExceededException("Вы исчерпали бесплатный лимит аватарок.");
            }

            ArweaveConfig arConfig = loadArweaveConfig();
            String txId = postAvatarTransaction(arConfig, cleanLogin, cleanType, fileBytes);

            int usedAfter = before.usedCount() + 1;
            int remainingAfter = Math.max(0, before.limit() - usedAfter);
            quotaDao.upsertUsage(cleanLogin, usedAfter, System.currentTimeMillis(), txId);
            return new UploadResult(txId, actualSha256Hex, usedAfter, remainingAfter, before.limit(), arConfig.gateway());
        }
    }

    public boolean isEnabled() {
        return config.getBoolean("test.freeAvatar.enabled", true);
    }

    public int getLimitPerUser() {
        int configured = config.getInt("test.freeAvatar.limitPerUser", DEFAULT_LIMIT);
        return Math.max(1, configured);
    }

    public int getMaxBytes() {
        int configured = config.getInt("test.freeAvatar.maxBytes", DEFAULT_MAX_BYTES);
        return Math.max(1024, configured);
    }

    private String postAvatarTransaction(ArweaveConfig arConfig, String login, String contentType, byte[] data)
            throws IOException, InterruptedException, GeneralSecurityException {
        String gateway = arConfig.gateway();
        String anchor = getRequiredText(gateway, "/tx_anchor");
        String reward = getRequiredText(gateway, "/price/" + data.length);
        if (!reward.matches("^\\d+$")) {
            throw new IllegalStateException("Arweave gateway вернул некорректную цену загрузки.");
        }

        if (!arConfig.address().isBlank()) {
            String balance = getRequiredText(gateway, "/wallet/" + arConfig.address() + "/balance");
            if (balance.matches("^\\d+$")) {
                BigInteger balanceWinston = new BigInteger(balance);
                BigInteger rewardWinston = new BigInteger(reward);
                if (balanceWinston.compareTo(rewardWinston) < 0) {
                    throw new IllegalStateException("На серверном Arweave-кошельке недостаточно AR для бесплатной загрузки аватара.");
                }
            }
        }

        byte[] dataRoot = singleChunkDataRoot(data);
        List<List<byte[]>> tagList = new ArrayList<>();
        List<Map<String, String>> jsonTags = new ArrayList<>();
        appendTag(jsonTags, tagList, "Content-Type", contentType);
        appendTag(jsonTags, tagList, "App-Name", "SHiNE");
        appendTag(jsonTags, tagList, "SHiNE-Type", "avatar-free-test");
        appendTag(jsonTags, tagList, "SHiNE-Profile-Login", login);

        byte[] signaturePayload = deepHash(List.of(
                utf8("2"),
                b64UrlDecode(arConfig.owner()),
                new byte[0],
                utf8("0"),
                utf8(reward),
                b64UrlDecode(anchor),
                tagList,
                utf8(Integer.toString(data.length)),
                dataRoot
        ));

        byte[] rawSignature = signPayload(arConfig.privateKey(), signaturePayload);
        String txId = b64UrlEncode(sha256(rawSignature));

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("format", 2);
        body.put("id", txId);
        body.put("last_tx", anchor);
        body.put("owner", arConfig.owner());
        body.put("tags", jsonTags);
        body.put("target", "");
        body.put("quantity", "0");
        body.put("data_root", b64UrlEncode(dataRoot));
        body.put("data_size", Integer.toString(data.length));
        body.put("data", b64UrlEncode(data));
        body.put("reward", reward);
        body.put("signature", b64UrlEncode(rawSignature));

        String bodyJson = MAPPER.writeValueAsString(body);
        HttpRequest req = HttpRequest.newBuilder(URI.create(gateway + "/tx"))
                .timeout(Duration.ofSeconds(40))
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(bodyJson, StandardCharsets.UTF_8))
                .build();
        HttpResponse<String> response = HTTP.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        int status = response.statusCode();
        if (status != 200 && status != 208) {
            throw new IllegalStateException("Arweave отклонил транзакцию: HTTP " + status + " " + safeBody(response.body()));
        }
        return txId;
    }

    private ArweaveConfig loadArweaveConfig() throws IOException, GeneralSecurityException {
        String rawGateway = String.valueOf(config.getParam("test.freeAvatar.gateway") == null
                ? DEFAULT_GATEWAY
                : config.getParam("test.freeAvatar.gateway")).trim();
        String gateway = rawGateway.isBlank() ? DEFAULT_GATEWAY : rawGateway.replaceAll("/+$", "");

        String walletPathRaw = String.valueOf(config.getParam("test.freeAvatar.walletJwkPath") == null
                ? ""
                : config.getParam("test.freeAvatar.walletJwkPath")).trim();
        if (walletPathRaw.isBlank()) {
            throw new IllegalStateException("Не задан test.freeAvatar.walletJwkPath в настройках сервера.");
        }

        JsonNode jwk = MAPPER.readTree(Files.readString(Path.of(walletPathRaw), StandardCharsets.UTF_8));
        String owner = requiredText(jwk, "n");
        PrivateKey privateKey = buildPrivateKeyFromJwk(jwk);
        String computedAddress = b64UrlEncode(sha256(b64UrlDecode(owner)));

        String expectedAddress = String.valueOf(config.getParam("test.freeAvatar.walletAddress") == null
                ? ""
                : config.getParam("test.freeAvatar.walletAddress")).trim();
        if (!expectedAddress.isBlank() && !expectedAddress.equals(computedAddress)) {
            throw new IllegalStateException("test.freeAvatar.walletAddress не совпадает с адресом из JWK.");
        }

        return new ArweaveConfig(gateway, owner, computedAddress, privateKey);
    }

    private static PrivateKey buildPrivateKeyFromJwk(JsonNode jwk) throws GeneralSecurityException {
        RSAPrivateCrtKeySpec spec = new RSAPrivateCrtKeySpec(
                asBigInt(requiredText(jwk, "n")),
                asBigInt(requiredText(jwk, "e")),
                asBigInt(requiredText(jwk, "d")),
                asBigInt(requiredText(jwk, "p")),
                asBigInt(requiredText(jwk, "q")),
                asBigInt(requiredText(jwk, "dp")),
                asBigInt(requiredText(jwk, "dq")),
                asBigInt(requiredText(jwk, "qi"))
        );
        return KeyFactory.getInstance("RSA").generatePrivate(spec);
    }

    private static byte[] signPayload(PrivateKey key, byte[] payload) throws GeneralSecurityException {
        Signature signature = Signature.getInstance("RSASSA-PSS");
        signature.setParameter(new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1));
        signature.initSign(key);
        signature.update(payload);
        return signature.sign();
    }

    private static byte[] singleChunkDataRoot(byte[] data) throws GeneralSecurityException {
        byte[] dataHash = sha256(data);
        byte[] left = sha256(dataHash);
        byte[] right = sha256(intToBuffer32(data.length));
        return sha256(concat(left, right));
    }

    @SuppressWarnings("unchecked")
    private static byte[] deepHash(Object data) throws GeneralSecurityException {
        if (data instanceof List<?> list) {
            byte[] tag = concat(utf8("list"), utf8(Integer.toString(list.size())));
            return deepHashChunks(list, sha384(tag));
        }
        if (!(data instanceof byte[] bytes)) {
            throw new IllegalArgumentException("deepHash поддерживает только byte[] и list.");
        }
        byte[] tag = concat(utf8("blob"), utf8(Integer.toString(bytes.length)));
        byte[] tagged = concat(sha384(tag), sha384(bytes));
        return sha384(tagged);
    }

    private static byte[] deepHashChunks(List<?> chunks, byte[] acc) throws GeneralSecurityException {
        if (chunks.isEmpty()) return acc;
        byte[] pair = concat(acc, deepHash(chunks.get(0)));
        return deepHashChunks(chunks.subList(1, chunks.size()), sha384(pair));
    }

    private static void appendTag(List<Map<String, String>> jsonTags, List<List<byte[]>> tagList, String name, String value) {
        byte[] nameBytes = utf8(name);
        byte[] valueBytes = utf8(value);
        Map<String, String> item = new LinkedHashMap<>();
        item.put("name", b64UrlEncode(nameBytes));
        item.put("value", b64UrlEncode(valueBytes));
        jsonTags.add(item);
        tagList.add(List.of(nameBytes, valueBytes));
    }

    private static String getRequiredText(String gateway, String path) throws IOException, InterruptedException {
        HttpRequest req = HttpRequest.newBuilder(URI.create(gateway + path))
                .timeout(Duration.ofSeconds(20))
                .GET()
                .build();
        HttpResponse<String> response = HTTP.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
        if (response.statusCode() < 200 || response.statusCode() >= 300) {
            throw new IllegalStateException("Arweave gateway вернул HTTP " + response.statusCode() + " для " + path);
        }
        String body = String.valueOf(response.body() == null ? "" : response.body()).trim();
        if (body.isBlank()) {
            throw new IllegalStateException("Arweave gateway вернул пустой ответ для " + path);
        }
        return body;
    }

    private void validatePayload(String contentType, byte[] fileBytes) {
        if (fileBytes == null || fileBytes.length == 0) {
            throw new IllegalArgumentException("Файл аватара пустой.");
        }
        if (fileBytes.length > getMaxBytes()) {
            throw new IllegalArgumentException("Файл слишком большой для бесплатной загрузки. Максимум " + getMaxBytes() + " байт.");
        }
        if (!isSupportedContentType(contentType)) {
            throw new IllegalArgumentException("Поддерживаются только JPEG, PNG или WebP.");
        }
    }

    private static boolean isSupportedContentType(String contentType) {
        return "image/jpeg".equals(contentType)
                || "image/png".equals(contentType)
                || "image/webp".equals(contentType);
    }

    private static String normalizeContentType(String contentType) {
        return String.valueOf(contentType == null ? "" : contentType).trim().toLowerCase();
    }

    private static String requiredText(JsonNode node, String field) {
        String value = node == null ? "" : String.valueOf(node.path(field).asText("")).trim();
        if (value.isBlank()) {
            throw new IllegalStateException("В JWK отсутствует поле " + field + ".");
        }
        return value;
    }

    private static BigInteger asBigInt(String b64Url) {
        return new BigInteger(1, b64UrlDecode(b64Url));
    }

    private static byte[] b64UrlDecode(String value) {
        return B64URL.decode(String.valueOf(value == null ? "" : value).trim());
    }

    private static String b64UrlEncode(byte[] value) {
        return B64URL_NOPAD.encodeToString(value);
    }

    private static byte[] utf8(String value) {
        return String.valueOf(value == null ? "" : value).getBytes(StandardCharsets.UTF_8);
    }

    private static byte[] concat(byte[]... arrays) {
        int total = 0;
        for (byte[] array : arrays) total += array.length;
        byte[] out = new byte[total];
        int offset = 0;
        for (byte[] array : arrays) {
            System.arraycopy(array, 0, out, offset, array.length);
            offset += array.length;
        }
        return out;
    }

    private static byte[] intToBuffer32(int value) {
        byte[] out = new byte[32];
        long current = Integer.toUnsignedLong(value);
        for (int i = out.length - 1; i >= 0; i--) {
            out[i] = (byte) (current & 0xffL);
            current >>>= 8;
        }
        return out;
    }

    private static byte[] sha256(byte[] data) throws GeneralSecurityException {
        return MessageDigest.getInstance("SHA-256").digest(data);
    }

    private static byte[] sha384(byte[] data) throws GeneralSecurityException {
        return MessageDigest.getInstance("SHA-384").digest(data);
    }

    private static String sha256Hex(byte[] data) throws GeneralSecurityException {
        byte[] hash = sha256(data);
        StringBuilder sb = new StringBuilder(hash.length * 2);
        for (byte b : hash) sb.append(String.format("%02x", b));
        return sb.toString();
    }

    private static String safeBody(String body) {
        String text = String.valueOf(body == null ? "" : body).replace('\n', ' ').replace('\r', ' ').trim();
        if (text.length() <= 220) return text;
        return text.substring(0, 220) + "...";
    }

    public record Quota(int limit, int usedCount, int remainingCount, boolean enabled) {
    }

    public record UploadResult(String txId, String sha256Hex, int usedCount, int remainingCount, int limit, String gateway) {
    }

    private record ArweaveConfig(String gateway, String owner, String address, PrivateKey privateKey) {
    }

    public static final class FreeAvatarLimitExceededException extends RuntimeException {
        public FreeAvatarLimitExceededException(String message) {
            super(message);
        }
    }
}
