/**
 * Trust V1 state builder.
 *
 * Pure function that walks an ordered sequence of trust records and
 * accumulates the trust state: active/revoked keys and writer bindings.
 *
 * No I/O, no side effects, no infrastructure imports.
 * Accepts sync arrays or async streams (from TrustChainPort).
 *
 * @module domain/trust/TrustStateBuilder
 * @see docs/specs/TRUST_CRYPTO_ALGORITHM.md Section 11
 */
import TrustError from "../errors/TrustError.js";
import TrustReadonlyMap from "./TrustReadonlyMap.js";
const KEY_ID_PATTERN = /^ed25519:[a-f0-9]{64}$/;
const BINDING_KEY_SEPARATOR = '\0';
const TRUST_STATE_INVALID_CODE = 'E_TRUST_STATE_INVALID';
// -- TrustState ---------------------------------------------------------------
class TrustState {
    activeKeys;
    revokedKeys;
    writerBindings;
    revokedBindings;
    errors;
    recordsProcessed;
    constructor(fields) {
        this.activeKeys = copyActiveKeys(fields.activeKeys);
        this.revokedKeys = copyRevokedKeys(fields.revokedKeys);
        this.writerBindings = copyWriterBindings(fields.writerBindings);
        this.revokedBindings = copyRevokedBindings(fields.revokedBindings);
        this.errors = copyBuildErrors(fields.errors);
        assertRecordCount(fields.recordsProcessed);
        this.recordsProcessed = fields.recordsProcessed;
        Object.freeze(this);
    }
    hasActiveKey(keyId) {
        assertKeyId(keyId, 'keyId');
        return this.activeKeys.has(keyId);
    }
    getBindingsForWriter(writerId) {
        assertWriterId(writerId, 'writerId');
        const bindings = [];
        for (const [bindingKey, binding] of this.writerBindings) {
            const parsed = parseBindingKey(bindingKey, 'writerBindings');
            if (parsed.writerId === writerId) {
                bindings.push(binding);
            }
        }
        return Object.freeze(bindings);
    }
    hasRevokedBindingsForWriter(writerId) {
        assertWriterId(writerId, 'writerId');
        for (const bindingKey of this.revokedBindings.keys()) {
            if (parseBindingKey(bindingKey, 'revokedBindings').writerId === writerId) {
                return true;
            }
        }
        return false;
    }
}
function assertMap(value, field) {
    if (!(value instanceof Map)) {
        throw new TrustError(`${field} must be a Map`, {
            code: TRUST_STATE_INVALID_CODE,
            context: { field },
        });
    }
}
function assertArray(value, field) {
    if (!Array.isArray(value)) {
        throw new TrustError(`${field} must be an array`, {
            code: TRUST_STATE_INVALID_CODE,
            context: { field },
        });
    }
}
function assertNonEmptyString(value, field) {
    if (typeof value !== 'string' || value.length === 0) {
        throw new TrustError(`${field} must be a non-empty string`, {
            code: TRUST_STATE_INVALID_CODE,
            context: { field },
        });
    }
}
function assertKeyId(keyId, field) {
    assertNonEmptyString(keyId, field);
    if (!KEY_ID_PATTERN.test(keyId)) {
        throw new TrustError(`${field} must be an ed25519 key id`, {
            code: TRUST_STATE_INVALID_CODE,
            context: { field, keyId },
        });
    }
}
function assertWriterId(writerId, field) {
    assertNonEmptyString(writerId, field);
    if (writerId.includes(BINDING_KEY_SEPARATOR)) {
        throw new TrustError(`${field} must not contain the trust binding separator`, {
            code: TRUST_STATE_INVALID_CODE,
            context: { field, writerId },
        });
    }
}
function assertRecordCount(recordsProcessed) {
    if (!Number.isInteger(recordsProcessed) || recordsProcessed < 0) {
        throw new TrustError('recordsProcessed must be a non-negative integer', {
            code: TRUST_STATE_INVALID_CODE,
            context: { field: 'recordsProcessed' },
        });
    }
}
function encodeBindingKey(writerId, keyId) {
    assertWriterId(writerId, 'writerId');
    assertKeyId(keyId, 'keyId');
    return `${writerId}${BINDING_KEY_SEPARATOR}${keyId}`;
}
function parseBindingKey(bindingKey, field) {
    assertNonEmptyString(bindingKey, field);
    const separatorIndex = bindingKey.indexOf(BINDING_KEY_SEPARATOR);
    if (separatorIndex <= 0 || separatorIndex !== bindingKey.lastIndexOf(BINDING_KEY_SEPARATOR)) {
        throw new TrustError(`${field} key must encode exactly one writer/key binding`, {
            code: TRUST_STATE_INVALID_CODE,
            context: { field },
        });
    }
    const writerId = bindingKey.slice(0, separatorIndex);
    const keyId = bindingKey.slice(separatorIndex + BINDING_KEY_SEPARATOR.length);
    assertWriterId(writerId, `${field}.writerId`);
    assertKeyId(keyId, `${field}.keyId`);
    return { writerId, keyId };
}
function copyActiveKeys(source) {
    assertMap(source, 'activeKeys');
    const copy = new Map();
    for (const [keyId, info] of source) {
        assertKeyId(keyId, 'activeKeys.key');
        copy.set(keyId, freezeActiveKeyInfo(info));
    }
    return new TrustReadonlyMap(copy);
}
function copyRevokedKeys(source) {
    assertMap(source, 'revokedKeys');
    const copy = new Map();
    for (const [keyId, info] of source) {
        assertKeyId(keyId, 'revokedKeys.key');
        copy.set(keyId, freezeRevokedKeyInfo(info));
    }
    return new TrustReadonlyMap(copy);
}
function copyWriterBindings(source) {
    assertMap(source, 'writerBindings');
    const copy = new Map();
    for (const [bindingKey, info] of source) {
        const parsed = parseBindingKey(bindingKey, 'writerBindings');
        const frozen = freezeBindingInfo(info);
        assertMatchingBindingKey(parsed.keyId, frozen.keyId, 'writerBindings');
        copy.set(bindingKey, frozen);
    }
    return new TrustReadonlyMap(copy);
}
function copyRevokedBindings(source) {
    assertMap(source, 'revokedBindings');
    const copy = new Map();
    for (const [bindingKey, info] of source) {
        const parsed = parseBindingKey(bindingKey, 'revokedBindings');
        const frozen = freezeRevokedBindingInfo(info);
        assertMatchingBindingKey(parsed.keyId, frozen.keyId, 'revokedBindings');
        copy.set(bindingKey, frozen);
    }
    return new TrustReadonlyMap(copy);
}
function assertMatchingBindingKey(encodedKeyId, valueKeyId, field) {
    if (encodedKeyId !== valueKeyId) {
        throw new TrustError(`${field} keyId must match the encoded binding key`, {
            code: TRUST_STATE_INVALID_CODE,
            context: { field, encodedKeyId, valueKeyId },
        });
    }
}
function assertEntryObject(value, field) {
    if (value === null || typeof value !== 'object') {
        throw new TrustError(`${field} entry must be an object`, {
            code: TRUST_STATE_INVALID_CODE,
            context: { field },
        });
    }
}
function freezeActiveKeyInfo(info) {
    assertEntryObject(info, 'activeKeys');
    assertNonEmptyString(info.publicKey, 'activeKeys.publicKey');
    assertNonEmptyString(info.addedAt, 'activeKeys.addedAt');
    return Object.freeze({ publicKey: info.publicKey, addedAt: info.addedAt });
}
function freezeRevokedKeyInfo(info) {
    assertEntryObject(info, 'revokedKeys');
    assertNonEmptyString(info.publicKey, 'revokedKeys.publicKey');
    assertNonEmptyString(info.revokedAt, 'revokedKeys.revokedAt');
    assertNonEmptyString(info.reasonCode, 'revokedKeys.reasonCode');
    return Object.freeze({ publicKey: info.publicKey, revokedAt: info.revokedAt, reasonCode: info.reasonCode });
}
function freezeBindingInfo(info) {
    assertEntryObject(info, 'writerBindings');
    assertKeyId(info.keyId, 'writerBindings.keyId');
    assertNonEmptyString(info.boundAt, 'writerBindings.boundAt');
    return Object.freeze({ keyId: info.keyId, boundAt: info.boundAt });
}
function freezeRevokedBindingInfo(info) {
    assertEntryObject(info, 'revokedBindings');
    assertKeyId(info.keyId, 'revokedBindings.keyId');
    assertNonEmptyString(info.revokedAt, 'revokedBindings.revokedAt');
    assertNonEmptyString(info.reasonCode, 'revokedBindings.reasonCode');
    return Object.freeze({ keyId: info.keyId, revokedAt: info.revokedAt, reasonCode: info.reasonCode });
}
function copyBuildErrors(errors) {
    assertArray(errors, 'errors');
    return Object.freeze(errors.map((error) => {
        assertEntryObject(error, 'errors');
        assertNonEmptyString(error.recordId, 'errors.recordId');
        assertNonEmptyString(error.error, 'errors.error');
        return Object.freeze({ recordId: error.recordId, error: error.error });
    }));
}
// -- Crypto validation --------------------------------------------------------
function validateFingerprint(subject, recordId, compute, errors) {
    try {
        const expected = compute(subject.publicKey);
        if (expected !== subject.keyId) {
            errors.push({ recordId, error: `KEY_ADD fingerprint mismatch: declared ${subject.keyId}, computed ${expected}` });
            return false;
        }
        return true;
    }
    catch (err) {
        errors.push({ recordId, error: `KEY_ADD fingerprint validation failed: ${err instanceof Error ? err.message : String(err)}` });
        return false;
    }
}
function resolveIssuerKey(rec, activeKeys) {
    if (rec.isKeyAdd() && rec.issuerKeyId === rec.subject.keyId) {
        return rec.subject.publicKey;
    }
    const found = activeKeys.get(rec.issuerKeyId);
    if (found && found.publicKey.length > 0) {
        return found.publicKey;
    }
    return null;
}
function validateCryptography(rec, ctx) {
    if (ctx.options.computeKeyFingerprint && rec.isKeyAdd()) {
        if (!validateFingerprint(rec.subject, rec.recordId, ctx.options.computeKeyFingerprint, ctx.errors)) {
            return false;
        }
    }
    if (ctx.options.signatureVerifier === undefined) {
        return true;
    }
    const issuerPk = resolveIssuerKey(rec, ctx.activeKeys);
    if (issuerPk === null) {
        ctx.errors.push({ recordId: rec.recordId, error: `Unknown issuer key for signature verification: ${rec.issuerKeyId}` });
        return false;
    }
    try {
        if (!ctx.options.signatureVerifier(rec, issuerPk)) {
            ctx.errors.push({ recordId: rec.recordId, error: `Signature verification failed for issuer key ${rec.issuerKeyId}` });
            return false;
        }
    }
    catch (err) {
        ctx.errors.push({ recordId: rec.recordId, error: `Signature verification failed: ${err instanceof Error ? err.message : String(err)}` });
        return false;
    }
    return true;
}
// -- Per-type record handlers -------------------------------------------------
function handleKeyAdd(subject, issuedAt, recordId, ctx) {
    if (ctx.revokedKeys.has(subject.keyId)) {
        ctx.errors.push({ recordId, error: `Cannot re-add revoked key: ${subject.keyId}` });
        return;
    }
    if (ctx.activeKeys.has(subject.keyId)) {
        ctx.errors.push({ recordId, error: `Duplicate KEY_ADD for already-active key: ${subject.keyId}` });
        return;
    }
    ctx.activeKeys.set(subject.keyId, { publicKey: subject.publicKey, addedAt: issuedAt });
}
function handleKeyRevoke(subject, issuedAt, recordId, ctx) {
    if (ctx.revokedKeys.has(subject.keyId)) {
        ctx.errors.push({ recordId, error: `Key already revoked: ${subject.keyId}` });
        return;
    }
    const keyInfo = ctx.activeKeys.get(subject.keyId);
    if (!keyInfo) {
        ctx.errors.push({ recordId, error: `Cannot revoke unknown key: ${subject.keyId}` }); // nosemgrep: ts-no-unknown-outside-adapters -- 0025B
        return;
    }
    ctx.activeKeys.delete(subject.keyId);
    ctx.revokedKeys.set(subject.keyId, { publicKey: keyInfo.publicKey, revokedAt: issuedAt, reasonCode: subject.reasonCode });
}
function handleBindAdd(subject, issuedAt, recordId, ctx) {
    const bindingKey = encodeBindingKey(subject.writerId, subject.keyId);
    if (ctx.revokedKeys.has(subject.keyId)) {
        ctx.errors.push({ recordId, error: `Cannot bind writer to revoked key: ${subject.keyId}` });
        return;
    }
    if (!ctx.activeKeys.has(subject.keyId)) {
        ctx.errors.push({ recordId, error: `Cannot bind writer to unknown key: ${subject.keyId}` }); // nosemgrep: ts-no-unknown-outside-adapters -- 0025B
        return;
    }
    ctx.writerBindings.set(bindingKey, { keyId: subject.keyId, boundAt: issuedAt });
}
function handleBindRevoke(subject, issuedAt, recordId, ctx) {
    const bindingKey = encodeBindingKey(subject.writerId, subject.keyId);
    const binding = ctx.writerBindings.get(bindingKey);
    if (!binding) {
        ctx.errors.push({ recordId, error: `Cannot revoke non-existent binding: writer=${subject.writerId} key=${subject.keyId}` });
        return;
    }
    ctx.writerBindings.delete(bindingKey);
    ctx.revokedBindings.set(bindingKey, { keyId: subject.keyId, revokedAt: issuedAt, reasonCode: subject.reasonCode });
}
// -- Record dispatch ----------------------------------------------------------
function processRecord(rec, ctx) {
    if (!validateCryptography(rec, ctx)) {
        return;
    }
    if (rec.isKeyAdd()) {
        handleKeyAdd(rec.subject, rec.issuedAt, rec.recordId, ctx);
    }
    else if (rec.isKeyRevoke()) {
        handleKeyRevoke(rec.subject, rec.issuedAt, rec.recordId, ctx);
    }
    else if (rec.isBindAdd()) {
        handleBindAdd(rec.subject, rec.issuedAt, rec.recordId, ctx);
    }
    else if (rec.isBindRevoke()) {
        handleBindRevoke(rec.subject, rec.issuedAt, rec.recordId, ctx);
    }
}
function isAsyncIterable(v) {
    return v !== null && v !== undefined && Symbol.asyncIterator in Object(v);
}
/**
 * Builds trust state from an ordered sequence of trust records.
 *
 * Records MUST be in chain order (oldest first). The builder enforces:
 * - Monotonic revocation: once a key is revoked, it cannot be re-added
 * - Binding validity: WRITER_BIND_ADD requires the referenced key to be active
 * - Cryptographic verification (when signatureVerifier is provided)
 *
 * Records are already validated by TrustRecord.fromDecoded() at the
 * adapter boundary. No Zod, no schema re-validation here.
 */
async function buildState(records, options = {}) {
    const ctx = {
        activeKeys: new Map(),
        revokedKeys: new Map(),
        writerBindings: new Map(),
        revokedBindings: new Map(),
        errors: [],
        options,
    };
    let count = 0;
    if (isAsyncIterable(records)) {
        for await (const record of records) {
            processRecord(record, ctx);
            count++;
        }
    }
    else {
        for (const record of records) {
            processRecord(record, ctx);
            count++;
        }
    }
    return new TrustState({
        activeKeys: ctx.activeKeys,
        revokedKeys: ctx.revokedKeys,
        writerBindings: ctx.writerBindings,
        revokedBindings: ctx.revokedBindings,
        errors: ctx.errors,
        recordsProcessed: count,
    });
}
export { TrustState, buildState };
