// T1.5 Phase 1 of the country-resilience reference-grade upgrade plan
// (docs/internal/country-resilience-upgrade-plan.md).
//
// Foundation-only slice: the staleness classifier. This module defines
// the cadence taxonomy (Realtime, Daily, Weekly, Monthly, Annual), the
// three-level staleness output (fresh, aging, stale), and a pure
// classifier function that maps a `lastObservedAt` timestamp and a
// source cadence to a staleness level.
//
// What is deliberately NOT in this module:
//
// - No changes to the 13 dimension scorers. Propagating `lastObservedAt`
//   through each scorer and aggregating max age per dimension is the
//   next slice of T1.5 and will depend on this classifier. Keeping the
//   classifier in its own module means that slice becomes a simple
//   consumer wiring pass with no test surface for the classifier itself.
// - No schema changes (proto, OpenAPI, ResilienceDimension response
//   type). The schema field `freshness: { lastObservedAt, staleness }`
//   lands alongside the widget rendering in T1.6 and consumes this
//   classifier.
// - No widget rendering. T1.6 owns the per-dimension freshness badge
//   UI and will call `classifyStaleness` from the widget path at render
//   time given the already-exposed `lastObservedAt` field.
//
// The multiplier thresholds below come from a simple rule: a source is
// fresh if its age is less than 1.5 times its cadence, aging if less
// than 3 times, stale otherwise. This scales gracefully across the 5
// cadences the methodology document lists without per-cadence ad-hoc
// numbers.

export type ResilienceCadence = 'realtime' | 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'annual';

export type StalenessLevel = 'fresh' | 'aging' | 'stale';

// Canonical cadence duration in milliseconds. A "unit" of each cadence.
// Realtime sources are expected to refresh within an hour; daily within
// a day; annual within a year. A simple, defensible base.
const CADENCE_UNIT_MS: Record<ResilienceCadence, number> = {
  realtime: 60 * 60 * 1000,                     // 1 hour
  daily: 24 * 60 * 60 * 1000,                   // 1 day
  weekly: 7 * 24 * 60 * 60 * 1000,              // 7 days
  monthly: 30 * 24 * 60 * 60 * 1000,            // 30 days
  quarterly: 91 * 24 * 60 * 60 * 1000,          // 91 days
  annual: 365 * 24 * 60 * 60 * 1000,            // 365 days
};

// Multiplier thresholds applied to the cadence unit. A source is fresh
// when its age is less than `FRESH_MULTIPLIER * cadenceUnit`, aging when
// less than `AGING_MULTIPLIER * cadenceUnit`, stale otherwise.
export const FRESH_MULTIPLIER = 1.5;
export const AGING_MULTIPLIER = 3;

export function cadenceUnitMs(cadence: ResilienceCadence): number {
  return CADENCE_UNIT_MS[cadence];
}

export interface ClassifyStalenessArgs {
  /** Unix milliseconds when the signal was last observed. */
  lastObservedAtMs: number | null | undefined;
  /** Cadence of the source publishing the signal. */
  cadence: ResilienceCadence;
  /** Override the current time for deterministic testing. Defaults to Date.now(). */
  nowMs?: number;
}

export interface StalenessResult {
  staleness: StalenessLevel;
  /**
   * Age in milliseconds. `Number.POSITIVE_INFINITY` when `lastObservedAtMs`
   * is null, undefined, NaN, or in the future. Always check for `Infinity`
   * (or use `Number.isFinite`) before using this value in arithmetic or
   * display formatting, otherwise downstream string concatenation will
   * silently emit `Infinity` and `NaN`.
   */
  ageMs: number;
  /**
   * The age expressed as a multiple of the cadence unit. Handy for
   * debugging. Same infinity contract as `ageMs`: returns
   * `Number.POSITIVE_INFINITY` in the defensive branches.
   */
  ageInCadenceUnits: number;
}

/**
 * Classify how fresh a signal is relative to its cadence.
 *
 * Returns `'stale'` when `lastObservedAtMs` is null, undefined, NaN, or
 * in the future. Returns `'fresh'` when age is strictly less than
 * `FRESH_MULTIPLIER * cadenceUnit`. Returns `'aging'` when age is
 * strictly less than `AGING_MULTIPLIER * cadenceUnit`. Returns `'stale'`
 * otherwise.
 *
 * The function is pure: same inputs, same outputs, no side effects.
 * `nowMs` is accepted for deterministic unit tests.
 */
export function classifyStaleness(args: ClassifyStalenessArgs): StalenessResult {
  const { lastObservedAtMs, cadence } = args;
  const nowMs = args.nowMs ?? Date.now();
  const unit = cadenceUnitMs(cadence);

  if (
    lastObservedAtMs == null ||
    !Number.isFinite(lastObservedAtMs) ||
    lastObservedAtMs > nowMs
  ) {
    return { staleness: 'stale', ageMs: Number.POSITIVE_INFINITY, ageInCadenceUnits: Number.POSITIVE_INFINITY };
  }

  // The defensive branch above already rejected null, undefined, NaN,
  // and future timestamps, so `nowMs - lastObservedAtMs` is guaranteed
  // to be >= 0 by the time execution reaches this line. No Math.max
  // clamp is needed. Removed in PR #2947 review.
  const ageMs = nowMs - lastObservedAtMs;
  const ageInCadenceUnits = ageMs / unit;

  let staleness: StalenessLevel;
  if (ageInCadenceUnits < FRESH_MULTIPLIER) {
    staleness = 'fresh';
  } else if (ageInCadenceUnits < AGING_MULTIPLIER) {
    staleness = 'aging';
  } else {
    staleness = 'stale';
  }

  return { staleness, ageMs, ageInCadenceUnits };
}
