// Guild access JWTs expire every 900s. Refresh ~2 min early to stay
// safely inside the window even if a tick runs late.
const TOKEN_TTL_MS = (15 - 2) * 60 * 1000;
export class PresenceSync {
    logger;
    client;
    timer = null;
    lastStatus = new Map(); // by agentId
    accounts = new Map();
    tokenCache = new Map(); // by agentId
    // Mutex flag: a tick iterates accounts serially with `await` on each
    // agent-login + PUT round-trip, so a single tick can easily run 20+s
    // when there are many accounts. setInterval(intervalMs) does NOT wait
    // for the previous tick to finish — without this guard the next tick
    // fires on top of a still-running one and two parallel iterations
    // PUT the same agentId within milliseconds. That tipped the backend's
    // first-time-insert race (separate fix in Fabric.Backend.Guild) into
    // 500s on prod. Guarded ticks just skip a beat instead.
    inflight = false;
    constructor(logger, client) {
        this.logger = logger;
        this.client = client;
    }
    setAccounts(accounts) {
        this.accounts.clear();
        for (const a of accounts)
            this.accounts.set(a.agentId, a);
    }
    start(intervalMs = 30_000) {
        if (this.timer)
            return;
        this.timer = setInterval(() => {
            this.tick().catch((err) => this.logger.warn(`fabric: presence-sync error: ${String(err)}`));
        }, intervalMs);
        // run once immediately so initial state lands fast
        void this.tick();
    }
    stop() {
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
        }
    }
    /**
     * Fetch a fresh guildAccessToken for `acct`, caching it under the
     * agentId until just before its JWT expiry. Returns null on login
     * failure or if the session has no matching guild — caller logs +
     * skips the PUT.
     */
    async ensureGuildToken(acct) {
        const now = Date.now();
        const cached = this.tokenCache.get(acct.agentId);
        if (cached && cached.expiresAt > now)
            return cached.token;
        let session;
        try {
            session = await this.client.agentLogin(acct.fabricApiKey);
        }
        catch (err) {
            this.logger.warn(`fabric: presence-sync agent-login failed for ${acct.agentId}: ${String(err)}`);
            return null;
        }
        const entry = session.guildAccessTokens.find((g) => g.guildNodeId === acct.guildNodeId);
        if (!entry?.token) {
            this.logger.warn(`fabric: presence-sync no guild token for ${acct.agentId} guild=${acct.guildNodeId}`);
            return null;
        }
        this.tokenCache.set(acct.agentId, { token: entry.token, expiresAt: now + TOKEN_TTL_MS });
        return entry.token;
    }
    async tick() {
        // Mutex: see the `inflight` field declaration for the why. Drop
        // overlapping ticks rather than letting them run concurrently —
        // status is gated by `lastStatus !== bridge.get`, so skipping a
        // beat costs nothing the next beat won't catch.
        if (this.inflight)
            return;
        this.inflight = true;
        try {
            await this.tickInner();
        }
        finally {
            this.inflight = false;
        }
    }
    async tickInner() {
        const bridge = globalThis['__hfAgentStatus'];
        if (!bridge || typeof bridge.get !== 'function')
            return; // HF plugin not loaded — skip
        for (const [agentId, acct] of this.accounts) {
            let status;
            try {
                status = await bridge.get(agentId);
            }
            catch {
                continue;
            }
            if (!status)
                continue;
            if (this.lastStatus.get(agentId) === status)
                continue; // no change → no PUT
            const guildToken = await this.ensureGuildToken(acct);
            if (!guildToken)
                continue;
            try {
                // Endpoint: PUT /api/agents/:userId/presence. ApiKeyGuard (global
                // APP_GUARD) requires `Authorization: Bearer <guildAccessToken>`
                // — NOT the agent's raw fabricApiKey. Pre-v1: this loop sent
                // x-api-key and got 401 "missing bearer token" forever. The /api
                // prefix is required because the guild backend sets a global
                // 'api' prefix in main.ts setGlobalPrefix('api').
                const url = `${acct.guildBaseUrl.replace(/\/$/, '')}/api/agents/${encodeURIComponent(acct.fabricUserId)}/presence`;
                const res = await fetch(url, {
                    method: 'PUT',
                    headers: {
                        'content-type': 'application/json',
                        authorization: `Bearer ${guildToken}`,
                    },
                    body: JSON.stringify({ status, source: 'hf-plugin' }),
                });
                if (res.ok) {
                    this.lastStatus.set(agentId, status);
                    this.logger.info(`fabric: presence-sync ${agentId} → ${status}`);
                }
                else {
                    // 401 here usually means the cached token went stale unexpectedly
                    // (server-side rotation or clock skew) — drop the cache so the
                    // next tick re-logs-in.
                    if (res.status === 401)
                        this.tokenCache.delete(agentId);
                    this.logger.warn(`fabric: presence-sync PUT ${agentId} failed: ${res.status}`);
                }
            }
            catch (err) {
                this.logger.warn(`fabric: presence-sync PUT ${agentId} threw: ${String(err)}`);
            }
        }
    }
}
