-- ============================================================
-- Asylum Compagnon — GearCompassChecksum.lua
-- Contrôle d'intégrité léger des exports GearCompass.
-- Adler-32 détecte une copie tronquée/modifiée mais ne chiffre ni ne signe.
-- ============================================================

local _, NS = ...

local Checksum = {}
NS.GearCompassChecksum = Checksum

local JSON = NS.GearCompassJSON
local MOD_ADLER = 65521
local REDUCE_EVERY = 5552

Checksum.ALGORITHM = "adler32"
Checksum.ENCODING = "utf-8"
Checksum.SCOPE = "raw_json_with_integrity_value_placeholder"
Checksum.PLACEHOLDER = "__GEARCOMPASS_CHECKSUM_PENDING__"

-- L'encodeur JSON trie les clés. Ce préfixe décrit donc sans ambiguïté le seul
-- emplacement dont la valeur est remplacée après calcul du checksum.
local VALUE_PREFIX = '"integrity":{"algorithm":"' .. Checksum.ALGORITHM
    .. '","encoding":"' .. Checksum.ENCODING
    .. '","scope":"' .. Checksum.SCOPE .. '","value":"'
Checksum.VALUE_PREFIX = VALUE_PREFIX

function Checksum.Adler32(text)
    if type(text) ~= "string" then return nil, "CHECKSUM_INPUT_NOT_STRING" end
    local a, b = 1, 0
    for index = 1, #text do
        a = a + string.byte(text, index)
        b = b + a
        if index % REDUCE_EVERY == 0 then
            a = a % MOD_ADLER
            b = b % MOD_ADLER
        end
    end
    a = a % MOD_ADLER
    b = b % MOD_ADLER
    return b * 65536 + a
end

function Checksum.Adler32Hex(text)
    local value, err = Checksum.Adler32(text)
    if value == nil then return nil, err end
    return string.format("%08x", value)
end

local function IntegrityWithValue(value)
    return {
        algorithm = Checksum.ALGORITHM,
        encoding = Checksum.ENCODING,
        scope = Checksum.SCOPE,
        value = value,
    }
end

-- Le préimage est le JSON final, sauf que `integrity.value` contient le
-- PLACEHOLDER fixe. Le site peut reconstruire exactement ces octets UTF-8 sans
-- réencoder le JSON ni reproduire les règles numériques de Lua.
function Checksum.Encode(root)
    if type(root) ~= "table" then return nil, "CHECKSUM_ROOT_NOT_TABLE" end
    if not (JSON and type(JSON.Encode) == "function") then
        return nil, "CHECKSUM_JSON_ENCODER_UNAVAILABLE"
    end

    root.integrity = IntegrityWithValue(Checksum.PLACEHOLDER)
    local preimage, encodeError = JSON.Encode(root)
    if not preimage then return nil, encodeError end

    local prefixStart = preimage:find(VALUE_PREFIX, 1, true)
    if not prefixStart then return nil, "CHECKSUM_PLACEHOLDER_NOT_FOUND" end
    if preimage:find(VALUE_PREFIX, prefixStart + #VALUE_PREFIX, true) then
        return nil, "CHECKSUM_PLACEHOLDER_NOT_UNIQUE"
    end

    local valueStart = prefixStart + #VALUE_PREFIX
    local valueEnd = valueStart + #Checksum.PLACEHOLDER - 1
    if preimage:sub(valueStart, valueEnd) ~= Checksum.PLACEHOLDER
        or preimage:sub(valueEnd + 1, valueEnd + 1) ~= '"' then
        return nil, "CHECKSUM_PLACEHOLDER_INVALID"
    end

    local digest, digestError = Checksum.Adler32Hex(preimage)
    if not digest then return nil, digestError end
    local encoded = preimage:sub(1, valueStart - 1) .. digest
        .. preimage:sub(valueEnd + 1)
    root.integrity = IntegrityWithValue(digest)
    return encoded, nil, digest
end

function Checksum.Verify(encoded)
    if type(encoded) ~= "string" then return false, "CHECKSUM_INPUT_NOT_STRING" end
    local prefixStart = encoded:find(VALUE_PREFIX, 1, true)
    if not prefixStart then return false, "CHECKSUM_MISSING" end
    if encoded:find(VALUE_PREFIX, prefixStart + #VALUE_PREFIX, true) then
        return false, "CHECKSUM_DUPLICATE"
    end

    local valueStart = prefixStart + #VALUE_PREFIX
    local valueEnd = valueStart + 7
    local actual = encoded:sub(valueStart, valueEnd)
    if #actual ~= 8 or not actual:match("^[0-9a-f]+$")
        or encoded:sub(valueEnd + 1, valueEnd + 1) ~= '"' then
        return false, "CHECKSUM_FORMAT_INVALID"
    end

    local preimage = encoded:sub(1, valueStart - 1) .. Checksum.PLACEHOLDER
        .. encoded:sub(valueEnd + 1)
    local expected, err = Checksum.Adler32Hex(preimage)
    if not expected then return false, err end
    if actual ~= expected then return false, "CHECKSUM_MISMATCH", expected, actual end
    return true, nil, expected, actual
end

function Checksum.SelfTest()
    local vector = Checksum.Adler32Hex("Wikipedia")
    local root = {
        schema_version = 2,
        export_type = "gearcompass",
        addon = { name = "AsylumCompagnon", version = "0.44.1" },
        character = { name = "Étoile" },
        items = JSON.Array({
            { item = { item_link = "|cnIQ4:|Hitem:123:0|h[Test]|h|r" } },
        }),
    }
    local encoded, encodeError, digest = Checksum.Encode(root)
    local valid, verifyError = Checksum.Verify(encoded)
    local mutated = encoded and encoded:gsub("AsylumCompagnon", "AsylumCompagnox", 1)
    local mutatedValid, mutatedError = Checksum.Verify(mutated)
    local truncatedValid = Checksum.Verify(encoded and encoded:sub(1, -2))
    local ok = vector == "11e60398"
        and type(encoded) == "string" and encodeError == nil
        and type(digest) == "string" and #digest == 8
        and valid == true and verifyError == nil
        and mutatedValid == false and mutatedError == "CHECKSUM_MISMATCH"
        and truncatedValid == false
        and encoded:find(Checksum.PLACEHOLDER, 1, true) == nil
        and encoded:find('"value":"' .. digest .. '"', 1, true) ~= nil
        and encoded:find("|", 1, true) == nil
        and encoded:find("\\u007ccnIQ4:", 1, true) ~= nil
        and encoded:find("\\u007cHitem:123:0", 1, true) ~= nil
        and encoded:find("%z") == nil and encoded:sub(-1) == "}"
    return not not ok, {
        vector = vector,
        encoded = encoded,
        digest = digest,
        wow_links_copy_safe = encoded and encoded:find("|", 1, true) == nil,
        mutated_error = mutatedError,
    }
end
