/**
 * Content Hashing - SHA256 hashing for conflict detection
 *
 * Computes content hashes for elements to detect actual content changes
 * during merge operations. Excludes identity and attribution fields.
 *
 * NOTE: metadata._externalSync is stripped before hashing. These sync
 * bookkeeping fields (lastPushedAt, lastPushedHash, etc.) change after
 * every push and would otherwise create a feedback loop where each push
 * changes the hash, triggering another push on the next daemon cycle.
 */
import { createHash } from 'crypto';
import { HASH_EXCLUDED_FIELDS } from './types.js';
// ============================================================================
// Content Hash Computation
// ============================================================================
/**
 * Compute content hash for an element
 *
 * Hash computation:
 * 1. Collect content fields (exclude identity/attribution)
 * 2. Sort object keys for determinism
 * 3. JSON stringify
 * 4. Prepend type for type-specific hashing
 * 5. SHA256 hash
 *
 * @param element - Element to hash
 * @returns Hash result with hex-encoded SHA256 hash
 */
export async function computeContentHash(element) {
    // Collect content fields
    const contentFields = {};
    const includedFields = [];
    for (const [key, value] of Object.entries(element)) {
        if (!HASH_EXCLUDED_FIELDS.includes(key)) {
            contentFields[key] = stripSyncBookkeeping(key, value);
            includedFields.push(key);
        }
    }
    // Sort keys for determinism
    const sortedFields = {};
    const sortedKeys = Object.keys(contentFields).sort();
    for (const key of sortedKeys) {
        sortedFields[key] = contentFields[key];
    }
    // Create hash input: type prefix + JSON content
    const hashInput = `${element.type}:${JSON.stringify(sortedFields, sortKeyReplacer)}`;
    // Compute SHA256 hash
    const hash = await sha256Hex(hashInput);
    return {
        hash,
        fields: includedFields.sort(),
    };
}
/**
 * Compute content hash synchronously using Node.js crypto
 *
 * Works in both Bun and Node.js runtimes.
 *
 * @param element - Element to hash
 * @returns Hash result with hex-encoded SHA256 hash
 */
export function computeContentHashSync(element) {
    // Collect content fields
    const contentFields = {};
    const includedFields = [];
    for (const [key, value] of Object.entries(element)) {
        if (!HASH_EXCLUDED_FIELDS.includes(key)) {
            contentFields[key] = stripSyncBookkeeping(key, value);
            includedFields.push(key);
        }
    }
    // Sort keys for determinism
    const sortedFields = {};
    const sortedKeys = Object.keys(contentFields).sort();
    for (const key of sortedKeys) {
        sortedFields[key] = contentFields[key];
    }
    // Create hash input: type prefix + JSON content
    const hashInput = `${element.type}:${JSON.stringify(sortedFields, sortKeyReplacer)}`;
    // Compute SHA256 hash using Node.js crypto (works in both Bun and Node.js)
    const hash = createHash('sha256').update(hashInput).digest('hex');
    return {
        hash,
        fields: includedFields.sort(),
    };
}
// ============================================================================
// Hash Comparison
// ============================================================================
/**
 * Check if two elements have the same content hash
 *
 * @param a - First element
 * @param b - Second element
 * @returns True if content hashes match
 */
export function hasSameContentHash(a, b) {
    const hashA = computeContentHashSync(a);
    const hashB = computeContentHashSync(b);
    return hashA.hash === hashB.hash;
}
/**
 * Compare pre-computed hash with element
 *
 * @param element - Element to check
 * @param expectedHash - Expected hash value
 * @returns True if hash matches
 */
export function matchesContentHash(element, expectedHash) {
    const result = computeContentHashSync(element);
    return result.hash === expectedHash;
}
// ============================================================================
// Helpers
// ============================================================================
/**
 * Strip sync bookkeeping fields from metadata before hashing.
 *
 * metadata._externalSync contains fields like lastPushedAt and lastPushedHash
 * that are updated after every push. Including them in the hash would create a
 * feedback loop: push → metadata changes → hash changes → looks dirty → push again.
 */
function stripSyncBookkeeping(key, value) {
    if (key === 'metadata' && typeof value === 'object' && value !== null) {
        const { _externalSync, ...restMetadata } = value;
        return restMetadata;
    }
    return value;
}
/**
 * JSON replacer that sorts object keys for deterministic stringification
 */
function sortKeyReplacer(_key, value) {
    if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
        const sorted = {};
        for (const k of Object.keys(value).sort()) {
            sorted[k] = value[k];
        }
        return sorted;
    }
    return value;
}
/**
 * Compute SHA256 hash of a string, returning hex-encoded result
 * Uses Web Crypto API for async operation
 */
async function sha256Hex(input) {
    const encoder = new TextEncoder();
    const data = encoder.encode(input);
    const hashBuffer = await crypto.subtle.digest('SHA-256', data);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}
//# sourceMappingURL=hash.js.map