package com.r35157.nenjim.service.journal.valuetypes;

import org.apache.commons.codec.binary.Base32;

import java.util.Objects;
import java.util.regex.Pattern;

/** A validated content identity supported by Journal format version 1. */
public record ContentDigest(String scheme, String value) {
    public static final String SHA256 = "sha256";
    public static final String CID1 = "cid1";

    private static final Pattern SHA256_VALUE = Pattern.compile("[0-9a-f]{64}");
    private static final Pattern CID1_VALUE = Pattern.compile("b[a-z2-7]+");
    private static final String BASE32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";

    public ContentDigest {
        Objects.requireNonNull(scheme, "scheme");
        Objects.requireNonNull(value, "value");
        switch (scheme) {
            case SHA256 -> {
                if (!SHA256_VALUE.matcher(value).matches()) {
                    throw new IllegalArgumentException("Invalid SHA-256 digest: '" + value + "'");
                }
            }
            case CID1 -> validateCidV1(value);
            default -> throw new IllegalArgumentException("Unsupported content digest scheme: '" + scheme + "'");
        }
    }

    public static ContentDigest parse(String text) {
        Objects.requireNonNull(text, "text");
        int separator = text.indexOf(':');
        if (separator <= 0 || separator == text.length() - 1 || text.indexOf(':', separator + 1) >= 0) {
            throw new IllegalArgumentException("Content digest must have the form <scheme>:<value>: '" + text + "'");
        }
        return new ContentDigest(text.substring(0, separator), text.substring(separator + 1));
    }

    public static ContentDigest sha256(String hexadecimalDigest) {
        return new ContentDigest(SHA256, hexadecimalDigest);
    }

    @Override
    public String toString() {
        return scheme + ":" + value;
    }

    private static void validateCidV1(String value) {
        if (!CID1_VALUE.matcher(value).matches()) {
            throw new IllegalArgumentException("Invalid CIDv1 base32 value: '" + value + "'");
        }

        byte[] decoded;
        try {
            decoded = new Base32().decode(value.substring(1));
        } catch (IllegalArgumentException exception) {
            throw new IllegalArgumentException("Invalid CIDv1 base32 value: '" + value + "'", exception);
        }
        if (decoded.length == 0) {
            throw new IllegalArgumentException("Invalid empty CIDv1 value");
        }
        String encoded = encodeCanonicalBase32(decoded);
        if (!value.substring(1).equals(encoded)) {
            throw new IllegalArgumentException("CIDv1 must use canonical unpadded lowercase Base32: '" + value + "'");
        }

        VarInt version = readVarInt(decoded, 0, "CID version");
        if (version.value != 1) {
            throw new IllegalArgumentException("CID must encode version 1: '" + value + "'");
        }
        VarInt codec = readVarInt(decoded, version.nextOffset, "CID multicodec");
        if (codec.value <= 0) {
            throw new IllegalArgumentException("CIDv1 multicodec must be positive: '" + value + "'");
        }
        VarInt hashCode = readVarInt(decoded, codec.nextOffset, "CID multihash code");
        if (hashCode.value <= 0) {
            throw new IllegalArgumentException("CIDv1 multihash code must be positive: '" + value + "'");
        }
        VarInt hashLength = readVarInt(decoded, hashCode.nextOffset, "CID multihash length");
        long remaining = decoded.length - hashLength.nextOffset;
        if (hashLength.value <= 0 || hashLength.value != remaining) {
            throw new IllegalArgumentException("CIDv1 multihash length does not match its digest: '" + value + "'");
        }
    }

    private static String encodeCanonicalBase32(byte[] bytes) {
        StringBuilder encoded = new StringBuilder((bytes.length * Byte.SIZE + 4) / 5);
        int buffer = 0;
        int bufferedBits = 0;
        for (byte current : bytes) {
            buffer = (buffer << Byte.SIZE) | Byte.toUnsignedInt(current);
            bufferedBits += Byte.SIZE;
            while (bufferedBits >= 5) {
                bufferedBits -= 5;
                encoded.append(BASE32_ALPHABET.charAt((buffer >>> bufferedBits) & 0x1f));
            }
            buffer = bufferedBits == 0 ? 0 : buffer & ((1 << bufferedBits) - 1);
        }
        if (bufferedBits > 0) {
            encoded.append(BASE32_ALPHABET.charAt((buffer << (5 - bufferedBits)) & 0x1f));
        }
        return encoded.toString();
    }

    private static VarInt readVarInt(byte[] bytes, int offset, String label) {
        long value = 0;
        for (int index = offset; index < bytes.length; index++) {
            int encodedBytes = index - offset;
            if (encodedBytes >= 9) {
                throw new IllegalArgumentException(label + " varint overflows the supported long range");
            }
            int current = Byte.toUnsignedInt(bytes[index]);
            int shift = encodedBytes * 7;
            int payload = current & 0x7f;
            if (payload > (Long.MAX_VALUE >>> shift)) {
                throw new IllegalArgumentException(label + " varint overflows the supported long range");
            }
            value |= (long) payload << shift;
            if ((current & 0x80) == 0) {
                if (encodedBytes + 1 != minimalVarIntLength(value)) {
                    throw new IllegalArgumentException(label + " uses a non-minimal varint encoding");
                }
                return new VarInt(value, index + 1);
            }
        }
        throw new IllegalArgumentException(label + " contains an unterminated varint");
    }

    private static int minimalVarIntLength(long value) {
        if (value == 0) {
            return 1;
        }
        int significantBits = Long.SIZE - Long.numberOfLeadingZeros(value);
        return (significantBits + 6) / 7;
    }

    private record VarInt(long value, int nextOffset) {
    }
}
