/**
 * Federation store (HUB side): which remote deployments have joined each team,
 * and the bots they last advertised. The hub aggregates these with its own local
 * roster so every member sees a shared cross-deployment roster.
 *
 * A deployment registers once via an invite (→ teamId) and is issued a long-lived
 * `syncToken` (high-entropy, never logged) used for subsequent sync/roster pulls.
 *
 * Storage: `{dataDir}/federations.json`, atomic writes. Single dashboard writer
 * (same assumption as invite-store): register/sync are read-modify-write.
 */
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from 'node:fs';
import { join } from 'node:path';
import { randomBytes, randomUUID } from 'node:crypto';
function filePath(dataDir) {
    return join(dataDir, 'federations.json');
}
function readFile(dataDir) {
    const fp = filePath(dataDir);
    if (!existsSync(fp))
        return { version: 1, teams: {} };
    try {
        const parsed = JSON.parse(readFileSync(fp, 'utf-8'));
        if (parsed && typeof parsed.teams === 'object' && parsed.teams)
            return { version: 1, teams: parsed.teams };
    }
    catch { /* corrupt — fall through */ }
    return { version: 1, teams: {} };
}
function writeFileAtomic(dataDir, data) {
    if (!existsSync(dataDir))
        mkdirSync(dataDir, { recursive: true });
    const fp = filePath(dataDir);
    const tmp = `${fp}.${process.pid}.${randomUUID()}.tmp`;
    writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', 'utf-8');
    renameSync(tmp, fp);
}
/**
 * Register a NEW remote deployment under a team and issue a fresh syncToken.
 *
 * NOT idempotent on purpose: `deploymentId` is public (it appears in the roster),
 * so re-registering an existing id must NOT hand back its long-lived syncToken —
 * otherwise anyone with an invite could claim another deployment's token by
 * guessing its id. A duplicate returns `{ created: false }` with NO token; the
 * caller surfaces `deployment_already_joined`. Ongoing bot refresh is via
 * `syncDeployment` (syncToken), not this. Re-binding/rotation is a future
 * explicit reset proving the old syncToken.
 */
export function registerDeployment(dataDir, teamId, input, now = Date.now()) {
    const data = readFile(dataDir);
    const list = data.teams[teamId] ?? (data.teams[teamId] = []);
    if (list.some(d => d.deploymentId === input.deploymentId))
        return { syncToken: '', created: false };
    const syncToken = randomBytes(24).toString('base64url');
    list.push({ deploymentId: input.deploymentId, name: input.name, syncToken, bots: input.bots, joinedAt: now, lastSeenAt: now, ownerUnionId: input.ownerUnionId, ownerName: input.ownerName, callbackUrl: input.callbackUrl, delegationToken: input.delegationToken });
    writeFileAtomic(dataDir, data);
    return { syncToken, created: true };
}
/** Resolve a syncToken to its {teamId, deployment}, or null. */
export function getDeploymentByToken(dataDir, syncToken) {
    if (!syncToken)
        return null;
    const data = readFile(dataDir);
    for (const [teamId, list] of Object.entries(data.teams)) {
        const deployment = list.find(d => d.syncToken === syncToken);
        if (deployment)
            return { teamId, deployment };
    }
    return null;
}
/** Refresh a deployment's advertised bots + heartbeat, by syncToken. Returns true if found. */
export function syncDeployment(dataDir, syncToken, bots, owner, now = Date.now()) {
    const data = readFile(dataDir);
    for (const list of Object.values(data.teams)) {
        const deployment = list.find(d => d.syncToken === syncToken);
        if (deployment) {
            deployment.bots = bots;
            deployment.lastSeenAt = now;
            // Owner + name can change after join (e.g. binding adopts the owner's
            // Feishu name) — keep them fresh on sync, only overwriting when sent.
            if (owner?.ownerUnionId !== undefined)
                deployment.ownerUnionId = owner.ownerUnionId;
            if (owner?.ownerName !== undefined)
                deployment.ownerName = owner.ownerName;
            if (owner?.name)
                deployment.name = owner.name; // non-empty only → roster grouping follows the owner's Feishu name
            writeFileAtomic(dataDir, data);
            return true;
        }
    }
    return false;
}
/** Member deployments of a team (empty if none). */
export function listFederatedDeployments(dataDir, teamId) {
    return readFile(dataDir).teams[teamId] ?? [];
}
/** Remove a deployment from a team (leave/kick). Returns true if removed. */
export function removeDeployment(dataDir, teamId, deploymentId) {
    const data = readFile(dataDir);
    const list = data.teams[teamId];
    if (!list)
        return false;
    const before = list.length;
    data.teams[teamId] = list.filter(d => d.deploymentId !== deploymentId);
    if (data.teams[teamId].length === before)
        return false;
    writeFileAtomic(dataDir, data);
    return true;
}
/** Remove the deployment owning a syncToken (spoke-initiated leave/revoke). Returns true if removed. */
export function removeDeploymentByToken(dataDir, syncToken) {
    if (!syncToken)
        return false;
    const data = readFile(dataDir);
    for (const list of Object.values(data.teams)) {
        const idx = list.findIndex(d => d.syncToken === syncToken);
        if (idx >= 0) {
            list.splice(idx, 1);
            writeFileAtomic(dataDir, data);
            return true;
        }
    }
    return false;
}
/** Drop all federation records for a team (e.g. when the team is deleted). */
export function removeTeamFederation(dataDir, teamId) {
    const data = readFile(dataDir);
    if (data.teams[teamId]) {
        delete data.teams[teamId];
        writeFileAtomic(dataDir, data);
    }
}
//# sourceMappingURL=federation-store.js.map