/**
 * Deterministic liveness computation (RFC-001 §4.5).
 *
 * The authoritative liveness signal is the `mosaic.presence` heartbeat, NOT
 * native Matrix presence. Given the age of an agent's last heartbeat and a
 * policy, these pure functions classify online/away/offline the same way every
 * time — which is exactly what makes the A3 "hard-killed agent flips to
 * offline within dark_threshold" guarantee deterministic and testable without
 * standing up a homeserver.
 */

import type {
  AgentLiveness,
  HeartbeatObservation,
  LivenessPolicy,
  PresenceStatus,
} from './types.js';

/**
 * Classify a single agent from the age (ms) of its last heartbeat.
 *
 * - `age <= heartbeatIntervalMs * missTolerance` → **online**
 * - `age <  darkThresholdMs`                      → **away**
 * - otherwise (or non-finite age)                 → **offline / dark**
 *
 * A non-finite age (never seen / NaN) fails safe to `offline`: we never assert
 * a liveness we cannot substantiate.
 */
export function classifyLiveness(ageMs: number, policy: LivenessPolicy): PresenceStatus {
  if (!Number.isFinite(ageMs)) {
    return 'offline';
  }
  const onlineWindowMs = policy.heartbeatIntervalMs * policy.missTolerance;
  if (ageMs <= onlineWindowMs) {
    return 'online';
  }
  if (ageMs < policy.darkThresholdMs) {
    return 'away';
  }
  return 'offline';
}

/**
 * Compute liveness for every observed agent at wall-clock `nowMs`.
 * The result order mirrors the input order (stable for display).
 */
export function computeFleetLiveness(
  observations: readonly HeartbeatObservation[],
  nowMs: number,
  policy: LivenessPolicy,
): AgentLiveness[] {
  return observations.map((o) => {
    const ageMs = nowMs - o.lastSeenTs;
    return {
      slug: o.slug,
      mxid: o.mxid,
      status: classifyLiveness(ageMs, policy),
      lastSeenTs: o.lastSeenTs,
      ageMs,
      lastSeq: o.lastSeq,
    };
  });
}
