import { createHash } from 'node:crypto';

import type { CheckDefinitionSpec, CheckSetPolicySpec } from './types.js';

// Deterministic JSON: object keys sorted at every level so two specs with the
// same content always produce the same bytes (and thus the same digest).
export function canonicalJson(value: unknown): string {
  if (value === null || typeof value !== 'object') {
    return JSON.stringify(value);
  }
  if (Array.isArray(value)) {
    return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
  }
  const record = value as Record<string, unknown>;
  const keys = Object.keys(record).sort();
  return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
}

/** sha256 over the canonical JSON of `value`. */
export function digestContent(value: unknown): string {
  return createHash('sha256').update(canonicalJson(value), 'utf8').digest('hex');
}

/**
 * Content digest of a check definition: covers the declarative spec (id,
 * version, canonical check, description, applicability, params) — everything a
 * reviewer reasons about — while excluding the executable function object.
 * Changing any covered field changes the digest, so a recorded digest always
 * identifies exactly which definition content produced a verdict.
 */
export function digestOfSpec(spec: CheckDefinitionSpec): string {
  return digestContent({
    id: spec.id,
    version: spec.version,
    canonicalCheck: spec.canonicalCheck,
    description: spec.description,
    appliesTo: spec.appliesTo,
    params: spec.params,
  });
}

/** Content digest of the per-subject check-set policy. */
export function digestOfPolicy(spec: CheckSetPolicySpec): string {
  return digestContent(spec);
}
