/**
 * @module db/familyStamp
 * @description The generalized FAMILY STAMP — one JSON shape that declares,
 * for any derived projection, WHICH source state it reflects and HOW to verify
 * it is whole. The entity tree (canonical current-state files) carries the
 * first brainy-side stamp; native index families carry the same shape. One
 * verifier reads both member modes:
 *
 * - `enumerated` — bounded families: exact byte size per member file,
 *   verified at open.
 * - `rollup`     — unbounded families (the entity tree: millions of files):
 *   the verified surface is a small set of rollup invariants (entity/
 *   relationship counts) plus `sourceGeneration`.
 *
 * `sourceGeneration` is the generation of the source-of-truth log this
 * projection reflects — open-time coherence becomes a COMPARISON (stamp vs
 * log head), not a walk:
 *
 * - equal + invariants hold → coherent, serve.
 * - behind → the projection missed the tail (crash between commit and stamp);
 *   for the Stage-1 tree this is benign by construction (the tree is written
 *   BY the commit), so the stamp refreshes; a DERIVED projection would replay
 *   the gap instead.
 * - invariants FAIL at equal generation → genuine incoherence: loud, and the
 *   repair ritual (`repairIndex()`, whose recount rebuilds the rollups from a
 *   canonical walk) heals it.
 *
 * Stamps are JSON on purpose — every incident gets debugged by reading a
 * stamp in a terminal.
 */

/** Storage-root-relative directory holding family stamps. */
export const FAMILY_STAMPS_PREFIX = '_system/family-stamps'

/** The entity tree's stamp path. */
export const ENTITY_TREE_STAMP_PATH = `${FAMILY_STAMPS_PREFIX}/entity-tree.json`

/** One enumerated member: a file and its exact expected byte size. */
export interface EnumeratedMember {
  path: string
  bytes: number
}

/**
 * The stamp's verified surface, in one of the two member modes. Rollup
 * invariant values may be numbers (counts, byte sizes) or strings (content
 * fingerprints, e.g. a per-tree SHA-256) — the verifier compares by strict
 * equality either way, so a type mismatch reads as incoherence, never a pass.
 */
export type StampMembers =
  | { mode: 'enumerated'; files: EnumeratedMember[] }
  | { mode: 'rollup'; invariants: Record<string, number | string> }

/** The generalized family stamp (one shape, one verifier, both engines). */
export interface FamilyStamp {
  /** Which projection this stamps (e.g. `'entity-tree'`). */
  family: string
  /** Monotonic per-family stamp generation — bumps on every committed stamp. */
  generation: number
  /** ISO timestamp of the stamp write. */
  committedAt: string
  /** The source-of-truth generation this projection reflects. */
  sourceGeneration: number
  /** The verified surface. */
  members: StampMembers
}

/** The verdict of an open-time stamp verification. */
export type StampVerdict =
  | { state: 'coherent' }
  | { state: 'absent' } // legacy store — first stamp writes at the next flush
  | { state: 'behind'; stampSource: number; head: number }
  | { state: 'incoherent'; failures: string[] }
  | { state: 'unverifiable'; reason: string } // a FAULT reading the stamp — never conflated with absence

/** The narrow storage surface stamps ride (JSON objects + fsync). */
export interface StampStorage {
  readRawObject(path: string): Promise<any | null>
  writeRawObject(path: string, data: any): Promise<void>
  syncRawObjects(paths: string[]): Promise<void>
}

/** Read a family's stamp; `null` when none was ever written. */
export async function readFamilyStamp(
  storage: StampStorage,
  path: string
): Promise<FamilyStamp | null> {
  const stored = (await storage.readRawObject(path)) as FamilyStamp | null
  if (!stored || typeof stored !== 'object' || typeof stored.family !== 'string') return null
  return stored
}

/** Write a family's stamp durably (atomic object write + fsync). */
export async function writeFamilyStamp(
  storage: StampStorage,
  path: string,
  stamp: Omit<FamilyStamp, 'generation' | 'committedAt'> & { generation?: number }
): Promise<void> {
  const prior = await readFamilyStamp(storage, path)
  const full: FamilyStamp = {
    ...stamp,
    generation: (prior?.generation ?? 0) + 1,
    committedAt: new Date().toISOString()
  }
  await storage.writeRawObject(path, full)
  await storage.syncRawObjects([path])
}

/**
 * The ONE verifier, both member modes. `actual` supplies the observed rollup
 * values (rollup mode) or file sizes (enumerated mode, keyed by path);
 * `head` is the source-of-truth generation now.
 */
export function verifyFamilyStamp(
  stamp: FamilyStamp | null,
  head: number,
  actual: Record<string, number | string>
): StampVerdict {
  if (stamp === null) return { state: 'absent' }
  if (stamp.sourceGeneration > head) {
    // A stamp AHEAD of the log claims state that never committed — the
    // projection was stamped against truth that a crash rolled back.
    return {
      state: 'incoherent',
      failures: [`sourceGeneration ${stamp.sourceGeneration} is ahead of the log head ${head}`]
    }
  }
  if (stamp.sourceGeneration < head) {
    return { state: 'behind', stampSource: stamp.sourceGeneration, head }
  }
  const failures: string[] = []
  if (stamp.members.mode === 'rollup') {
    for (const [name, expected] of Object.entries(stamp.members.invariants)) {
      const observed = actual[name]
      if (observed === undefined) {
        failures.push(`rollup invariant '${name}' has no observed value`)
      } else if (observed !== expected) {
        failures.push(`rollup invariant '${name}': stamped ${expected}, observed ${observed}`)
      }
    }
  } else {
    for (const member of stamp.members.files) {
      const observed = actual[member.path]
      if (observed === undefined) {
        failures.push(`member '${member.path}' is missing`)
      } else if (observed !== member.bytes) {
        failures.push(`member '${member.path}': stamped ${member.bytes} bytes, observed ${observed}`)
      }
    }
  }
  return failures.length > 0 ? { state: 'incoherent', failures } : { state: 'coherent' }
}
