/**
 * Server-saved diagnostics (GENC-1351 §5.8) — the forward-only stream that lets
 * the debug log persist across a whole session lifetime, not just the current
 * page load. This module owns the *pure* pieces: the entry type and the
 * reassembly back into the `{ readme, timeline, meta }` debug-log shape. The
 * element owns capture/cadence; the provider owns storage layout.
 *
 * @packageDocumentation
 */

/**
 * One entry in the diagnostics stream — the debug-log timeline-entry union
 * (`message` / `turn` / `event`, as `getDebugLog()` builds) plus a point-in-time
 * **`meta-snapshot`** carrying the export-time `meta` block (agent summary,
 * context, active debug snapshot, …). Kept permissive: a provider stores and
 * returns these opaquely, and reassembly reads only `kind`, `timestamp` (for
 * ordering), and `meta` (on a snapshot).
 *
 * @public
 */
export interface DiagnosticEntry {
  kind: 'message' | 'turn' | 'event' | 'meta-snapshot';
  /** ISO timestamp — string-comparable, so it orders the stream chronologically. */
  timestamp?: string;
  /** Present on `meta-snapshot` entries: the export-time `meta` block. */
  meta?: unknown;
  /**
   * Present on `meta-snapshot` entries: a stable signature of the near-static config
   * (excludes volatile timestamp/context/debug-snapshot), used by the forward-capture
   * delta to skip re-appending an unchanged block (GENC-1351 §5.8). Absent on other
   * kinds and on pre-signature entries (the delta falls back to the full `meta` then).
   */
  dedupSignature?: string;
  [key: string]: unknown;
}

/** The reassembled debug log — the exact shape `getDebugLog()` returns. @public */
export interface DebugLog {
  readme: readonly string[];
  timeline: DiagnosticEntry[];
  meta: unknown;
}

/**
 * Stable identity for a message diagnostic entry — `timestamp::role::subAgentOf`.
 * Used by the forward-capture delta (skip already-emitted messages) and by the
 * restore watermark (skip messages already present in a prior load's stream).
 * `null` when the entry has no timestamp → treat as always-new. Shared so the
 * element's diagnostics capture and the `SessionPersister`'s restore watermark
 * compute identity identically (GENC-1351 §5.8/§5.10).
 *
 * @internal
 */
export function diagnosticsMessageKey(entry: DiagnosticEntry): string | null {
  return typeof entry.timestamp === 'string'
    ? `${entry.timestamp}::${(entry.role as string) ?? ''}::${(entry.subAgentOf as string) ?? ''}`
    : null;
}

// Tie-break co-timestamped entries by cause → call → output (event, turn, message),
// matching the original in-place `getDebugLog` sort.
const KIND_RANK: Record<string, number> = { event: 0, turn: 1, message: 2 };
// Any non-{event,turn,message} kind (shouldn't reach the timeline — meta-snapshots
// are filtered out first) sorts last.
const UNKNOWN_KIND_RANK = 99;

/**
 * Reassemble diagnostic entries into the `{ readme, timeline, meta }` debug-log
 * shape (GENC-1351 §5.8). `timeline` = the `message`/`turn`/`event` entries sorted
 * by ISO `timestamp` (kind-rank tie-break). `meta` = the **newest** `meta-snapshot`'s
 * block only — a "state at export" photo, matching the single-page log. The older
 * snapshots are intentionally dropped: the per-turn evolution they would show is
 * already in the timeline (`turn.agentSnapshot` + `context.updated` events), so
 * keeping them would just repeat the bulky, near-static `agentSummary`. `readme` =
 * the passed (current) constant. Pure — reused for both the live current-page log
 * and the reassembled lifetime log stitched from the persisted stream, so both
 * come out shape-identical.
 */
export function assembleDebugLog(entries: DiagnosticEntry[], readme: readonly string[]): DebugLog {
  // Safety net for exact-duplicate entries: two element instances sharing one
  // session (bubble + popped-out panel) can each append the same entry, and a
  // reload's second instance can re-append restored history. Such copies are
  // byte-identical, so collapse by serialized identity — genuinely-distinct entries
  // (e.g. two real `assistant.connected` from two instances) differ in index /
  // placement and are kept. (The shared cursor store prevents most of this at
  // write time; this guards the read path regardless of what's in the stream.)
  const seen = new Set<string>();
  let latestMeta: DiagnosticEntry | undefined;
  const timeline: DiagnosticEntry[] = [];
  for (const entry of entries) {
    const id = JSON.stringify(entry);
    if (seen.has(id)) continue;
    seen.add(id);
    if (entry.kind === 'meta-snapshot') {
      if (!latestMeta || (entry.timestamp ?? '') >= (latestMeta.timestamp ?? '')) {
        latestMeta = entry;
      }
    } else {
      timeline.push(entry);
    }
  }
  timeline.sort((a, b) => {
    const ta = a.timestamp ?? '';
    const tb = b.timestamp ?? '';
    if (ta < tb) return -1;
    if (ta > tb) return 1;
    return (KIND_RANK[a.kind] ?? UNKNOWN_KIND_RANK) - (KIND_RANK[b.kind] ?? UNKNOWN_KIND_RANK);
  });
  return { readme, timeline, meta: latestMeta?.meta };
}
