import { createHash } from "node:crypto";

/**
 * Canonical JSON used by manifest schema v1.
 *
 * Object keys are sorted recursively, arrays retain their order, and values are
 * encoded with JSON.stringify. Undefined and non-JSON values are rejected.
 */
export function canonicalJson(value: unknown): string {
  return encode(value, new Set<object>());
}

function encode(value: unknown, seen: Set<object>): string {
  if (value === null || typeof value === "boolean" || typeof value === "string") {
    return JSON.stringify(value);
  }
  if (typeof value === "number") {
    if (!Number.isFinite(value)) {
      throw new TypeError("Canonical JSON refuses non-finite numbers");
    }
    return JSON.stringify(value);
  }
  if (typeof value !== "object") {
    throw new TypeError(`Canonical JSON refuses ${typeof value}`);
  }
  if (seen.has(value)) {
    throw new TypeError("Canonical JSON refuses cyclic objects");
  }
  seen.add(value);
  try {
    if (Array.isArray(value)) {
      return `[${value.map((entry) => encode(entry, seen)).join(",")}]`;
    }
    const record = value as Record<string, unknown>;
    const keys = Object.keys(record).sort();
    return `{${keys.map((key) => {
      const entry = record[key];
      if (entry === undefined) {
        throw new TypeError(`Canonical JSON refuses undefined at ${key}`);
      }
      return `${JSON.stringify(key)}:${encode(entry, seen)}`;
    }).join(",")}}`;
  } finally {
    seen.delete(value);
  }
}

export function sha256Hex(data: string | Uint8Array): string {
  return createHash("sha256").update(data).digest("hex");
}
