// Copyright 2024-2026 Tymofii Pidlisnyi. Apache-2.0 license. See LICENSE.
// ══════════════════════════════════════════════════════════════════
// CPA v0.1: producer (buildCPA) + content address (computeCpaRef)
// ══════════════════════════════════════════════════════════════════
// buildCPA groups a flat list of ContextItems by channel, builds one
// partition per non-empty channel, computes the top root over present
// partition roots in CHANNEL_ORDER, derives producer_pubkey from the
// private key, and signs the JCS-canonical form with signature emptied
// (the same canonical-bytes-then-sign pattern used by memory_provenance
// and instruction-provenance).
//
// computeCpaRef content-addresses the FULLY SIGNED object under SIGN_TAG.
// ══════════════════════════════════════════════════════════════════
import { createHash } from 'node:crypto';
import { canonicalizeJCS } from '../../core/canonical-jcs.js';
import { sign, publicKeyFromPrivate } from '../../crypto/keys.js';
import { CHANNEL_ORDER, } from './types.js';
import { SIGN_TAG, buildPartitionRootBytes, buildInclusionProof, buildTopRootBytes, bytesToHex, utf8Bytes, } from './merkle.js';
/**
 * Build and sign a CPA from a flat list of context items.
 *
 * full-set: each present partition carries leaves = ALL its leaves and no
 *   context_profile.
 * inclusion: each present partition carries context_profile
 *   { channel, hidden_leaf_count = leaf_count - disclosed }. Phase 0
 *   builds inclusion CPAs count-only (no disclosed leaves, so
 *   hidden_leaf_count = leaf_count); Phase 1 adds disclosed subsets with
 *   inclusion proofs.
 *
 * producer_pubkey is derived from the private key. The signature covers
 * canonicalizeJCS({...cpa, signature:''}).
 */
export function buildCPA(input) {
    const { privateKey, action_ref, producer_did, attested_at, mode, items } = input;
    // Normalize the inclusion-mode disclosure selector to a Set of ctx_ids.
    const disclose = input.disclose
        ? input.disclose instanceof Set
            ? input.disclose
            : new Set(input.disclose)
        : new Set();
    // Group items by channel, preserving every item.
    const byChannel = new Map();
    for (const item of items) {
        const bucket = byChannel.get(item.channel);
        if (bucket)
            bucket.push(item);
        else
            byChannel.set(item.channel, [item]);
    }
    // Build one partition per present channel, in CHANNEL_ORDER.
    const partitions = [];
    const partitionRootBytes = [];
    for (const channel of CHANNEL_ORDER) {
        const leaves = byChannel.get(channel);
        if (!leaves || leaves.length === 0)
            continue; // empty partitions omitted
        // partition_root is over ALL leaves, unchanged in either mode.
        const rootBytes = buildPartitionRootBytes(leaves);
        partitionRootBytes.push(rootBytes);
        const partition_root = bytesToHex(rootBytes);
        const leaf_count = leaves.length;
        if (mode === 'full-set') {
            partitions.push({ channel, partition_root, leaf_count, leaves: [...leaves] });
        }
        else {
            // inclusion mode. Disclose the chosen subset (if any) for this
            // partition: include each disclosed leaf (with its content if it
            // carried one) and its inclusion proof. Leaves not selected stay
            // hidden (count-only).
            const disclosedLeaves = [];
            const inclusion_proofs = [];
            for (const leaf of leaves) {
                if (disclose.has(leaf.ctx_id)) {
                    disclosedLeaves.push({ ...leaf });
                    inclusion_proofs.push(buildInclusionProof(leaves, leaf.ctx_id));
                }
            }
            const disclosedCount = disclosedLeaves.length;
            const partition = {
                channel,
                partition_root,
                leaf_count,
                context_profile: { channel, hidden_leaf_count: leaf_count - disclosedCount },
            };
            if (disclosedCount > 0) {
                // Disclosed leaves are emitted in the FROZEN ctx_id sort so the
                // wire order is deterministic and matches the tree ordering.
                disclosedLeaves.sort((a, b) => a.ctx_id < b.ctx_id ? -1 : a.ctx_id > b.ctx_id ? 1 : 0);
                inclusion_proofs.sort((a, b) => a.ctx_id < b.ctx_id ? -1 : a.ctx_id > b.ctx_id ? 1 : 0);
                partition.leaves = disclosedLeaves;
                partition.inclusion_proofs = inclusion_proofs;
            }
            partitions.push(partition);
        }
    }
    const root = bytesToHex(buildTopRootBytes(partitionRootBytes));
    const producer_pubkey = publicKeyFromPrivate(privateKey);
    const unsigned = {
        version: 'cpa/0.1',
        action_ref,
        producer_did,
        producer_pubkey,
        attested_at,
        mode,
        partitions,
        root,
        signature: '',
    };
    const bytes = canonicalizeJCS(unsigned);
    const signature = sign(bytes, privateKey);
    return { ...unsigned, signature };
}
/**
 * Content address over the FULLY SIGNED CPA:
 *   cpa_ref = sha256( utf8(SIGN_TAG) || utf8(canonicalizeJCS(signedCpa)) )
 * Returns 64-hex lowercase.
 */
export function computeCpaRef(signedCpa) {
    const h = createHash('sha256');
    h.update(utf8Bytes(SIGN_TAG));
    h.update(utf8Bytes(canonicalizeJCS(signedCpa)));
    return h.digest('hex');
}
// ══════════════════════════════════════════════════════════════════
// Mutual binding helpers (lean, module-local; see BINDING.md)
// ══════════════════════════════════════════════════════════════════
// CPA is a pure protocol primitive. It does NOT own receipt orchestration
// or any gateway state. To bind a CPA into a decision/completion receipt,
// a caller carries the CPA's content address (cpa_ref) as a field on the
// receipt. These helpers compute and attach that field WITHOUT pulling any
// receipt-signing or gateway logic into the primitive.
/**
 * Carry the content address of a signed CPA as a one-field object, ready
 * to spread onto a receipt body BEFORE the receipt is signed. The verifier
 * later re-derives computeCpaRef(cpa) and checks equality against the
 * receipt's cpa_ref (the receipt -> CPA binding direction).
 */
export function carryCpaRef(cpa) {
    return { cpa_ref: computeCpaRef(cpa) };
}
/**
 * Pure binder: return a NEW object equal to `receipt` plus a `cpa_ref`
 * field. Does not mutate the input and does not (re)sign anything; the
 * caller signs the resulting body with its own receipt machinery. Keeping
 * this pure keeps the primitive offline and side-effect free.
 */
export function bindCpaRefToReceipt(receipt, cpa_ref) {
    return { ...receipt, cpa_ref };
}
//# sourceMappingURL=cpa.js.map