/**
 * Deterministic canonical serialization helper for OrganizationModel snapshot hashing.
 *
 * Provides a stable JSON stringify that produces identical output for equivalent objects
 * regardless of key insertion order. Used by deploy-time and boot-time snapshot hash
 * verification to detect stale deployed OM snapshots.
 *
 * Only the serialization is here (browser-safe). Callers that need a hash compute it
 * themselves using their preferred crypto implementation (e.g. node:crypto sha256).
 */

import type { OrganizationModel } from './types'

/**
 * Recursively serialize a value to a deterministic JSON string with sorted keys.
 * Arrays preserve order; objects sort keys lexicographically.
 */
function deterministicStringify(value: unknown): string {
  if (Array.isArray(value)) {
    return `[${value.map(deterministicStringify).join(',')}]`
  }

  if (value !== null && typeof value === 'object') {
    return `{${Object.entries(value as Record<string, unknown>)
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([key, entry]) => `${JSON.stringify(key)}:${deterministicStringify(entry)}`)
      .join(',')}}`
  }

  return JSON.stringify(value)
}

/**
 * Produce a deterministic canonical string for an OrganizationModel suitable for hashing.
 *
 * Exclusions (volatile / self-referential fields that must not be part of the hash input):
 *   - `snapshotHash`   — the field being populated from this hash; must not be circular
 *   - `domainMetadata[*].lastModified` — wall-clock date string; changes on every edit
 *     independent of semantic content, causing spurious hash mismatches
 *
 * Same model content → same canonical string, regardless of key insertion order.
 */
export function canonicalStringifyOrganizationModel(model: OrganizationModel): string {
  // Shallow-clone to strip snapshotHash without mutating the caller's object.
  const { snapshotHash: _snapshotHash, domainMetadata, ...rest } = model

  // Strip lastModified from each domain metadata entry so volatile dates do not
  // pollute the hash. The domain version numbers are still included.
  const strippedDomainMetadata = domainMetadata
    ? Object.fromEntries(
        Object.entries(domainMetadata).map(([domain, meta]) => {
          if (meta === null || typeof meta !== 'object') return [domain, meta]
          const { lastModified: _lastModified, ...metaRest } = meta as Record<string, unknown>
          return [domain, metaRest]
        })
      )
    : undefined

  const forHashing = strippedDomainMetadata !== undefined ? { ...rest, domainMetadata: strippedDomainMetadata } : rest

  return deterministicStringify(forHashing)
}
