"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MeshClientRedisRegistry = void 0;
exports.destroyClientRedis = destroyClientRedis;
const __ΩReturnType = ['T', 'args', '', 'ReturnType', 'l>e"!R"RPde#!P"@2"h"!/#qk#\'QRb!Pde"!p)w$y'];
const redis_1 = require("../../helpers/redis/redis");
// --- Lua Scripts ---
// Safety-net TTL for all client registry keys. Primary cleanup is via
// unregister/cleanupNode; this only matters if ALL nodes crash without
// graceful shutdown and no leader ever runs cleanup.
const KEY_TTL_SECONDS = 86400; // 24 hours
// REGISTER atomically registers a client, optionally moving it between nodes.
// KEYS[1] = clients hash, KEYS[2] = new node's set
// ARGV[1] = clientId, ARGV[2] = nodeId, ARGV[3] = metadataJson,
// ARGV[4] = hasMetadata ("1" or "0"), ARGV[5] = setKeyPrefix,
// ARGV[6] = connectedAt, ARGV[7] = allowSupersede ("1" or "0"),
// ARGV[8] = state ("active" or "pending")
//
// Returns:
//   -1  = registered, no supersession (new client or re-register on same node)
//   -2  = conflict: another node owns this client and supersession not allowed
//   >=0 = registered, superseded from this nodeId
const REGISTER_SCRIPT = `
local clientsKey = KEYS[1]
local newSetKey = KEYS[2]
local clientId = ARGV[1]
local nodeId = ARGV[2]
local metadataJson = ARGV[3]
local hasMetadata = ARGV[4] == "1"
local setKeyPrefix = ARGV[5]
local connectedAt = tonumber(ARGV[6])
local allowSupersede = ARGV[7] == "1"
local state = ARGV[8]
local ttl = ${KEY_TTL_SECONDS}

-- Check if client already exists on a different node
local supersededNodeId = -1
local existing = redis.call("hget", clientsKey, clientId)
if existing then
    local parsed = cjson.decode(existing)
    local oldNodeId = tostring(parsed.nodeId)
    if oldNodeId ~= nodeId then
        if not allowSupersede then
            return -2
        end
        -- Construct old node's set key and remove
        local oldSetKey = setKeyPrefix .. oldNodeId .. ":clients"
        redis.call("srem", oldSetKey, clientId)
        supersededNodeId = tonumber(oldNodeId)
    end
end

-- Set in hash and add to new node's set
local value = cjson.encode({
    nodeId = tonumber(nodeId),
    connectedAt = connectedAt,
    state = state,
    hasMetadata = hasMetadata,
    metadata = cjson.decode(metadataJson)
})
redis.call("hset", clientsKey, clientId, value)
redis.call("sadd", newSetKey, clientId)

-- Refresh safety-net TTL on every write
redis.call("expire", clientsKey, ttl)
redis.call("expire", newSetKey, ttl)

-- Return old nodeId if client was superseded from a different node, else -1
return supersededNodeId
`;
// ACTIVATE promotes a same-node reservation from pending to active without
// re-taking ownership if the client moved elsewhere in the meantime.
// KEYS[1] = clients hash, KEYS[2] = node's set
// ARGV[1] = clientId, ARGV[2] = nodeId, ARGV[3] = metadataJson, ARGV[4] = hasMetadata ("1" or "0")
const ACTIVATE_SCRIPT = `
local clientsKey = KEYS[1]
local setKey = KEYS[2]
local clientId = ARGV[1]
local nodeId = ARGV[2]
local metadataJson = ARGV[3]
local hasMetadata = ARGV[4] == "1"
local ttl = ${KEY_TTL_SECONDS}

local existing = redis.call("hget", clientsKey, clientId)
if not existing then
    return 0
end

local parsed = cjson.decode(existing)
if tostring(parsed.nodeId) ~= nodeId then
    return 0
end

local value = cjson.encode({
    nodeId = parsed.nodeId,
    connectedAt = parsed.connectedAt,
    state = "active",
    hasMetadata = hasMetadata,
    metadata = cjson.decode(metadataJson)
})
redis.call("hset", clientsKey, clientId, value)
redis.call("sadd", setKey, clientId)
redis.call("expire", clientsKey, ttl)
redis.call("expire", setKey, ttl)
return 1
`;
const UNREGISTER_SCRIPT = `
local clientsKey = KEYS[1]
local setKey = KEYS[2]
local clientId = ARGV[1]
local nodeId = ARGV[2]

local existing = redis.call("hget", clientsKey, clientId)
if not existing then
    return 0
end

local parsed = cjson.decode(existing)
if tostring(parsed.nodeId) ~= nodeId then
    return 0
end

redis.call("hdel", clientsKey, clientId)
redis.call("srem", setKey, clientId)
return 1
`;
// UPDATE_METADATA atomically updates metadata only if the client is owned by the given node.
// KEYS[1] = clients hash
// ARGV[1] = clientId, ARGV[2] = nodeId, ARGV[3] = metadataJson, ARGV[4] = hasMetadata ("1" or "0")
const UPDATE_METADATA_SCRIPT = `
local clientsKey = KEYS[1]
local clientId = ARGV[1]
local nodeId = ARGV[2]
local metadataJson = ARGV[3]
local hasMetadata = ARGV[4] == "1"
local ttl = ${KEY_TTL_SECONDS}

local existing = redis.call("hget", clientsKey, clientId)
if not existing then
    return 0
end

local parsed = cjson.decode(existing)
if tostring(parsed.nodeId) ~= nodeId then
    return 0
end

local value = cjson.encode({
    nodeId = parsed.nodeId,
    connectedAt = parsed.connectedAt,
    state = parsed.state or "active",
    hasMetadata = hasMetadata,
    metadata = cjson.decode(metadataJson)
})
redis.call("hset", clientsKey, clientId, value)
redis.call("expire", clientsKey, ttl)
return 1
`;
const CLEANUP_NODE_SCRIPT = `
local clientsKey = KEYS[1]
local setKey = KEYS[2]
local nodeId = ARGV[1]

local members = redis.call("smembers", setKey)
local removed = {}

for _, clientId in ipairs(members) do
    local existing = redis.call("hget", clientsKey, clientId)
    if existing then
        local parsed = cjson.decode(existing)
        if tostring(parsed.nodeId) == nodeId then
            redis.call("hdel", clientsKey, clientId)
            table.insert(removed, existing)
            table.insert(removed, clientId)
        end
    end
end

redis.call("del", setKey)
return removed
`;
const __ΩClientRedisClient = [() => __ΩReturnType, () => redis_1.createRedis, "client", 'clientsKey', 'newSetKey', 'clientId', 'nodeId', 'metadataJson', 'hasMetadata', 'setKeyPrefix', 'connectedAt', 'allowSupersede', 'state', '', 'MC_REGISTER', 'setKey', 'MC_ACTIVATE', 'MC_UNREGISTER', 'MC_UPDATE_METADATA', 'MC_CLEANUP_NODE', 'ClientRedisClient', 'Pi"o!".#fPP&2$&2%&2&&2\'&2(&2)&2*&2+&2,&2-\'`/.4/P&2$&20&2&&2\'&2(&2)\'`/.41P&2$&20&2&&2\'\'`/.42P&2$&2&&2\'&2(&2)\'`/.43P&2$&20&2\'&F`/.44MKw5y'];
let clientRedis = null;
function getClientRedis() {
    if (!clientRedis) {
        const { client, prefix } = (0, redis_1.createRedis)('MESH');
        client.defineCommand('MC_REGISTER', { lua: REGISTER_SCRIPT, numberOfKeys: 2 });
        client.defineCommand('MC_ACTIVATE', { lua: ACTIVATE_SCRIPT, numberOfKeys: 2 });
        client.defineCommand('MC_UNREGISTER', { lua: UNREGISTER_SCRIPT, numberOfKeys: 2 });
        client.defineCommand('MC_UPDATE_METADATA', { lua: UPDATE_METADATA_SCRIPT, numberOfKeys: 1 });
        client.defineCommand('MC_CLEANUP_NODE', { lua: CLEANUP_NODE_SCRIPT, numberOfKeys: 2 });
        clientRedis = { client: client, prefix };
    }
    return clientRedis;
}
getClientRedis.__type = [() => __ΩClientRedisClient, 'client', 'prefix', 'getClientRedis', 'PPn!4"&4#M/$'];
function destroyClientRedis() {
    if (clientRedis) {
        clientRedis.client.disconnect();
        clientRedis = null;
    }
}
destroyClientRedis.__type = ['destroyClientRedis', 'P$/!'];
// --- MeshClientRedisRegistry ---
class MeshClientRedisRegistry {
    constructor(key) {
        this.key = key;
    }
    clientsKey() {
        const { prefix } = getClientRedis();
        return `${prefix}:mesh:${this.key}:clients`;
    }
    nodeSetKey(nodeId) {
        const { prefix } = getClientRedis();
        return `${prefix}:mesh:${this.key}:node:${nodeId}:clients`;
    }
    nodeSetKeyPrefix() {
        const { prefix } = getClientRedis();
        return `${prefix}:mesh:${this.key}:node:`;
    }
    encodeMetadata(metadata) {
        if (metadata === undefined) {
            return { metadataJson: 'null', hasMetadata: '0' };
        }
        const metadataJson = JSON.stringify(metadata);
        if (metadataJson === undefined) {
            throw new Error('Mesh client metadata must be JSON-serializable');
        }
        return { metadataJson, hasMetadata: '1' };
    }
    async registerWithState(clientId, nodeId, metadata, allowSupersede, state) {
        const { client } = getClientRedis();
        const encoded = this.encodeMetadata(metadata);
        const result = await client.MC_REGISTER(this.clientsKey(), this.nodeSetKey(nodeId), clientId, String(nodeId), encoded.metadataJson, encoded.hasMetadata, this.nodeSetKeyPrefix(), String(Date.now()), allowSupersede ? '1' : '0', state);
        if (result === -2) {
            // Conflict: the owner may have disappeared between the script
            // result and this follow-up read, so ownerNodeId can be null.
            const existing = await client.hget(this.clientsKey(), clientId);
            const parsed = existing ? this.tryParse(existing) : undefined;
            return { status: 'conflict', ownerNodeId: parsed?.nodeId ?? null };
        }
        return { status: 'ok', supersededNodeId: result >= 0 ? result : null };
    }
    async register(clientId, nodeId, metadata, allowSupersede = true) {
        return this.registerWithState(clientId, nodeId, metadata, allowSupersede, 'active');
    }
    async reserve(clientId, nodeId, metadata, allowSupersede = true) {
        return this.registerWithState(clientId, nodeId, metadata, allowSupersede, 'pending');
    }
    async activate(clientId, nodeId, metadata) {
        const { client } = getClientRedis();
        const encoded = this.encodeMetadata(metadata);
        const result = await client.MC_ACTIVATE(this.clientsKey(), this.nodeSetKey(nodeId), clientId, String(nodeId), encoded.metadataJson, encoded.hasMetadata);
        return result === 1;
    }
    async unregister(clientId, nodeId) {
        const { client } = getClientRedis();
        const result = await client.MC_UNREGISTER(this.clientsKey(), this.nodeSetKey(nodeId), clientId, String(nodeId));
        return result === 1;
    }
    async updateMetadata(clientId, nodeId, metadata) {
        const { client } = getClientRedis();
        const encoded = this.encodeMetadata(metadata);
        const result = await client.MC_UPDATE_METADATA(this.clientsKey(), clientId, String(nodeId), encoded.metadataJson, encoded.hasMetadata);
        return result === 1;
    }
    tryParse(raw) {
        try {
            const parsed = JSON.parse(raw);
            return {
                nodeId: parsed.nodeId,
                connectedAt: parsed.connectedAt,
                metadata: parsed.hasMetadata === false ? undefined : parsed.metadata,
                state: parsed.state ?? 'active'
            };
        }
        catch {
            return undefined;
        }
    }
    toRegisteredClient(clientId, parsed) {
        if (parsed.state !== 'active') {
            return undefined;
        }
        return {
            clientId,
            nodeId: parsed.nodeId,
            connectedAt: parsed.connectedAt,
            metadata: parsed.metadata
        };
    }
    async getClient(clientId) {
        const { client } = getClientRedis();
        const raw = await client.hget(this.clientsKey(), clientId);
        if (!raw)
            return undefined;
        const parsed = this.tryParse(raw);
        if (!parsed)
            return undefined;
        return this.toRegisteredClient(clientId, parsed);
    }
    async listClients() {
        const { client } = getClientRedis();
        const all = await client.hgetall(this.clientsKey());
        const results = [];
        for (const [clientId, raw] of Object.entries(all)) {
            const parsed = this.tryParse(raw);
            if (parsed) {
                const registered = this.toRegisteredClient(clientId, parsed);
                if (registered) {
                    results.push(registered);
                }
            }
        }
        return results;
    }
    async listClientsForNode(nodeId) {
        const { client } = getClientRedis();
        const clientIds = await client.smembers(this.nodeSetKey(nodeId));
        if (clientIds.length === 0)
            return [];
        const values = await client.hmget(this.clientsKey(), ...clientIds);
        const results = [];
        for (let i = 0; i < clientIds.length; i++) {
            const raw = values[i];
            if (raw) {
                const parsed = this.tryParse(raw);
                if (parsed && parsed.nodeId === nodeId) {
                    const registered = this.toRegisteredClient(clientIds[i], parsed);
                    if (registered) {
                        results.push(registered);
                    }
                }
            }
        }
        return results;
    }
    async cleanupNode(nodeId) {
        const { client } = getClientRedis();
        const result = await client.MC_CLEANUP_NODE(this.clientsKey(), this.nodeSetKey(nodeId), String(nodeId));
        // Result is alternating [json, clientId, json, clientId, ...]
        const removed = [];
        for (let i = 0; i < result.length; i += 2) {
            const parsed = this.tryParse(result[i]);
            if (parsed) {
                removed.push({
                    clientId: result[i + 1],
                    nodeId: parsed.nodeId,
                    connectedAt: parsed.connectedAt,
                    metadata: parsed.metadata
                });
            }
        }
        return removed;
    }
}
exports.MeshClientRedisRegistry = MeshClientRedisRegistry;
MeshClientRedisRegistry.__type = ['TMeta', 'key', 'constructor', 'clientsKey', 'nodeId', 'nodeSetKey', 'nodeSetKeyPrefix', 'metadata', 'metadataJson', "0", "1", 'hasMetadata', 'encodeMetadata', 'clientId', 'allowSupersede', 'MeshClientRegistrationState', 'state', 'RegisterResult', 'registerWithState', 'register', 'reserve', 'activate', 'unregister', 'updateMetadata', 'raw', 'connectedAt', 'tryParse', 'parsed', 'RegisteredClient', 'toRegisteredClient', 'getClient', 'listClients', 'listClientsForNode', 'cleanupNode', 'MeshClientRegistryBackend', 'MeshClientRedisRegistry', 'b!&3";P&2""0#P&0$;P\'2%&0&;P&0\';Pe"!2(P&4)P.*.+J4,M0-;P&2.\'2%e"!2()2/"w021"w2`03;P&2.\'2%e"!2("2/"w2`04P&2.\'2%e"!2("2/"w2`05P&2.\'2%e"!2()`06P&2.\'2%)`07P&2.\'2%e"!2()`08P&29PP\'4%\'4:e$!4("w041M-J0;;P&2.P\'4%\'4:e#!4("w041M2<P"w=-J0>;P&2.P"w=-J`0?P"w=F`0@P\'2%"w=F`0AP\'2%"w=F`0B5"wCx"wD'];
//# sourceMappingURL=mesh-client-redis-registry.js.map