/**
 * CredentialRebalancerSnapshot — pure mappers from the live system's state into the
 * balancer's read-only pass snapshot (Increment B, step B3b wiring helper).
 *
 * Spec: docs/specs/live-credential-repointing-rebalancer.md §2.4.
 *
 * The CredentialRebalancer orchestrator (B3a) consumes injected `listSlots` / `listAccounts`
 * / `resolveConfig` providers. These pure functions ARE those providers — they translate
 * the CredentialLocationLedger assignments and the SubscriptionPool accounts into the
 * policy's `SlotState` / `AccountState`, and clamp the configured balancer knobs into the
 * resolved config. Kept pure + out of server.ts so the mapping (the place a units/sign
 * bug would silently mis-steer the balancer) is unit-testable.
 */
const HOUR_MS = 3600_000;
/**
 * Map the pool account lifecycle status into the policy's eligibility status.
 * Only needs-reauth/disabled are INELIGIBLE (dead credentials). rate-limited is `ok`:
 * a walled account must stay eligible so wall-avoidance can RESCUE its slot (its high
 * utilization % drives the wall objective; excluding it would strand the very slot that
 * needs help).
 */
export function mapAccountStatus(s) {
    switch (s) {
        case 'needs-reauth': return 'needs-reauth';
        case 'disabled': return 'disabled';
        case 'active':
        case 'warming':
        case 'rate-limited':
        default:
            return 'ok';
    }
}
function pctOrNull(v) {
    return typeof v === 'number' && Number.isFinite(v) ? v : null;
}
function hoursUntil(resetsAt, now) {
    if (!resetsAt)
        return null;
    const t = Date.parse(resetsAt);
    if (Number.isNaN(t))
        return null;
    return Math.max(0, (t - now) / HOUR_MS);
}
/** One pool account → the policy's AccountState. */
export function mapAccount(account, now) {
    const q = account.lastQuota ?? null;
    const measuredAt = q?.measuredAt ? Date.parse(q.measuredAt) : NaN;
    return {
        accountId: account.id,
        status: mapAccountStatus(account.status),
        fiveHrPct: pctOrNull(q?.fiveHour?.utilizationPct),
        weeklyPct: pctOrNull(q?.sevenDay?.utilizationPct),
        weeklyResetsInHours: hoursUntil(q?.sevenDay?.resetsAt, now),
        // No quota reading at all ⇒ measuredAt 0 (epoch) ⇒ always STALE ⇒ source-only.
        measuredAt: Number.isNaN(measuredAt) ? 0 : measuredAt,
    };
}
export function mapAccounts(accounts, now) {
    return accounts.map((a) => mapAccount(a, now));
}
/** One ledger assignment → the policy's SlotState. */
export function mapSlot(a, opts) {
    const lastVerifiedAt = a.lastVerifiedAt ? Date.parse(a.lastVerifiedAt) : NaN;
    return {
        slot: a.slot,
        // The ledger uses '' for a quarantined-but-unassigned slot; normalize to null.
        tenantAccountId: a.accountId ? a.accountId : null,
        isDefault: opts.defaultSlot != null && a.slot === opts.defaultSlot,
        quarantined: a.quarantined,
        lastVerifiedAt: Number.isNaN(lastVerifiedAt) ? null : lastVerifiedAt,
        lastAuditDivergent: opts.auditDivergentBySlot?.[a.slot] ?? false,
        drainInProgress: opts.drainInProgressBySlot?.[a.slot] ?? false,
        busyness: opts.busynessBySlot?.[a.slot] ?? 0,
    };
}
export function mapSlots(assignments, opts) {
    return assignments.map((a) => mapSlot(a, opts));
}
/**
 * Per-slot busyness = count of RUNNING claude-code sessions on each slot (the drain
 * "busiest slot" signal, §2.4 — the balancer drains toward the busiest slot so an
 * expiring weekly window is spent where the work actually is). A session's slot is its
 * account's CURRENT slot (`slotOf(subscriptionAccountId)`); an UNTAGGED session (no pool
 * account — the default interactive session) counts toward the default slot. Non-running
 * or non-claude-code sessions don't count (only claude-code sessions read the credential
 * store the balancer steers). Pure; the live wiring passes `state.listSessions()`.
 */
export function computeBusynessBySlot(sessions, slotOf, defaultSlot) {
    const busy = {};
    for (const s of sessions) {
        if (s.status !== 'running')
            continue;
        // Undefined framework = a legacy record (pre-framework-tag) — treat as claude-code (the
        // default); only an EXPLICIT non-claude framework is excluded.
        if (s.framework !== undefined && s.framework !== 'claude-code')
            continue;
        const slot = s.subscriptionAccountId ? slotOf(s.subscriptionAccountId) : defaultSlot;
        if (slot)
            busy[slot] = (busy[slot] ?? 0) + 1;
    }
    return busy;
}
function clamp(v, lo, hi, dflt) {
    const n = typeof v === 'number' && Number.isFinite(v) ? v : dflt;
    return Math.min(hi, Math.max(lo, n));
}
/**
 * Clamp the configured knobs into the resolved config the orchestrator/policy use.
 * Cooldowns are DERIVED from the poll interval (per-pair ≥1×, per-tenant ≥2× — the
 * lag-sensored controller floors, §2.4); stale-quota is N poll periods.
 */
export function resolveRebalancerConfig(raw) {
    const passIntervalMs = clamp(raw.passIntervalMs, 60_000, 3_600_000, 300_000);
    const slotCount = Math.max(1, raw.slotCount ?? 1);
    const stalePeriods = clamp(raw.staleQuotaPollPeriods, 1, 10, 2);
    return {
        policy: {
            highWaterPct: clamp(raw.highWaterPct, 50, 99, 85),
            criticalPct: clamp(raw.criticalPct, 85, 99, 95),
            drainHorizonHours: clamp(raw.drainHorizonHours, 1, 96, 24),
            drainHeadroomMinPct: clamp(raw.drainHeadroomMinPct, 0, 100, 30),
            minScoreDelta: clamp(raw.minScoreDelta, 0, 1000, 10),
            maxForcedSwapsPerPass: clamp(raw.maxForcedSwapsPerPass, 1, slotCount, 1),
            perPairCooldownMs: passIntervalMs, // ≥1× poll interval
            perTenantCooldownMs: passIntervalMs * 2, // ≥2× poll interval (defeats 3-way rotation)
            staleQuotaMs: passIntervalMs * stalePeriods,
            urgencyClampHours: clamp(raw.urgencyClampHours, 1, 24, 4),
        },
        auditCadenceMs: clamp(raw.auditCadenceMs, HOUR_MS, 24 * HOUR_MS, 6 * HOUR_MS),
        desiredDefaultAccountId: raw.desiredDefaultAccountId ?? null,
        maxForcedOverridesPerWindow: clamp(raw.maxForcedOverridesPerWindow, 1, 100, 5),
        breakerThreshold: clamp(raw.breakerThreshold, 1, 20, 3),
    };
}
//# sourceMappingURL=CredentialRebalancerSnapshot.js.map