/**
 * BatchModeGovernor -- Amortised governance gate for high-volume classification.
 *
 * A classification agent processing 1,000 documents per cycle pays governance
 * overhead 1,000 times under per-call gating.  Batch mode amortises that cost:
 * gate ONCE per batch with composite metadata, then process all items.
 *
 * Opt-in semantics:
 *   - Operator must explicitly configure allowed tool names (no wildcards).
 *   - Pack binding is mandatory on every call -- empty binding throws.
 *   - Strict packs (HIPAA, GDPR, PCI DSS, HMDA, DORA) allow low-risk only.
 *   - Non-strict packs allow low-risk and medium-risk.
 *   - High-risk batches ALWAYS throw regardless of pack -- no escape hatch.
 *   - Batch size ceiling is 1,000 items (governance-overhead amortisation
 *     breakeven); operators may configure lower but not higher.
 *
 * Compliance bindings:
 *   - SOC 2 CC6.1: access control -- operator-explicit allowlist
 *   - HIPAA 164.312(a)(1): minimum-necessary; low-risk only under strict pack
 *   - GDPR Art. 5(1)(c): data minimisation; risk-profiled gating
 *   - PCI DSS Req 7: restrict access by business need
 *   - EU AI Act Art. 9: risk management; risk-profile declaration required
 *   - ISO 42001 A.6.1.4: resource management for AI systems
 *
 * BUILD_PLAN_V3_FULL_LIFECYCLE.md line 275: "Batch-mode governance for
 * high-volume classification (reduced per-call overhead)".
 *
 * Related siblings:
 *   - PR #14: tool-rate-limiter (per-call gate, consulted atomically per batch)
 *   - PR #12/#30: cycle-token-budget (batch reports Sigma tokens once)
 *   - PR #26: post-tool-audit-enrichment (redaction patterns mirrored here)
 *   - PR #33: skip-api-empty-queue (operator-supplied adapter pattern)
 *
 * @connexum/ai-governance
 */
import * as crypto from 'crypto';
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
export class BatchModeNotAllowedError extends Error {
    agentId;
    reason;
    packBinding;
    constructor(opts) {
        super(`Batch mode not allowed for agent '${opts.agentId}': ${opts.reason}`);
        this.name = 'BatchModeNotAllowedError';
        this.agentId = opts.agentId;
        this.reason = opts.reason;
        this.packBinding = opts.packBinding;
    }
}
export class BatchSizeExceededError extends Error {
    agentId;
    requested;
    limit;
    packBinding;
    constructor(opts) {
        super(`Batch size ${opts.requested} exceeds configured limit of ${opts.limit} ` +
            `for agent '${opts.agentId}'.`);
        this.name = 'BatchSizeExceededError';
        this.agentId = opts.agentId;
        this.requested = opts.requested;
        this.limit = opts.limit;
        this.packBinding = opts.packBinding;
    }
}
export class ToolNotInBatchAllowlistError extends Error {
    agentId;
    toolName;
    allowedToolNames;
    packBinding;
    constructor(opts) {
        super(`Tool '${opts.toolName}' is not in the batch allowlist for agent '${opts.agentId}'. ` +
            `Allowed: [${opts.allowedToolNames.join(', ')}].`);
        this.name = 'ToolNotInBatchAllowlistError';
        this.agentId = opts.agentId;
        this.toolName = opts.toolName;
        this.allowedToolNames = opts.allowedToolNames;
        this.packBinding = opts.packBinding;
    }
}
export class BatchModeRestrictedByPackError extends Error {
    agentId;
    riskProfile;
    restrictingPacks;
    constructor(opts) {
        super(`Batch mode with risk profile '${opts.riskProfile}' is restricted by ` +
            `strict pack(s) [${opts.restrictingPacks.join(', ')}] for agent '${opts.agentId}'. ` +
            `Strict packs permit low-risk batches only.`);
        this.name = 'BatchModeRestrictedByPackError';
        this.agentId = opts.agentId;
        this.riskProfile = opts.riskProfile;
        this.restrictingPacks = opts.restrictingPacks;
    }
}
export class BatchHighRiskForbiddenError extends Error {
    agentId;
    packBinding;
    constructor(opts) {
        super(`High-risk batch mode is unconditionally forbidden for agent '${opts.agentId}'. ` +
            `No pack override exists for this restriction.`);
        this.name = 'BatchHighRiskForbiddenError';
        this.agentId = opts.agentId;
        this.packBinding = opts.packBinding;
    }
}
export class BatchHeterogeneousRiskError extends Error {
    agentId;
    batchRiskProfile;
    itemIndex;
    constructor(opts) {
        super(`PT-BATCH-1: Heterogeneous-risk attack detected. ` +
            `Batch declares risk profile '${opts.batchRiskProfile}' but item at index ${opts.itemIndex} ` +
            `carries a per-item riskProfile override. Per-item risk overrides are rejected to prevent ` +
            `slipping high-risk items through low-risk gating for agent '${opts.agentId}'.`);
        this.name = 'BatchHeterogeneousRiskError';
        this.agentId = opts.agentId;
        this.batchRiskProfile = opts.batchRiskProfile;
        this.itemIndex = opts.itemIndex;
    }
}
export class EmptyPackBindingError extends Error {
    agentId;
    constructor(agentId) {
        super(`Pack binding is mandatory. Agent '${agentId}' submitted a batch with an empty packBinding. ` +
            `All batch operations must declare at least one compliance pack.`);
        this.name = 'EmptyPackBindingError';
        this.agentId = agentId;
    }
}
export class BatchNotConfiguredError extends Error {
    agentId;
    constructor(agentId) {
        super(`Batch mode has not been configured for agent '${agentId}'. ` +
            `Call configure() before guardPreBatch() or recordBatchResult().`);
        this.name = 'BatchNotConfiguredError';
        this.agentId = agentId;
    }
}
export class BatchIdNotFoundError extends Error {
    batchId;
    agentId;
    constructor(opts) {
        super(`Batch '${opts.batchId}' not found or already completed for agent '${opts.agentId}'. ` +
            `Call guardPreBatch() first.`);
        this.name = 'BatchIdNotFoundError';
        this.batchId = opts.batchId;
        this.agentId = opts.agentId;
    }
}
export class BatchToolNameDivergedError extends Error {
    agentId;
    batchId;
    declaredToolName;
    divergedItemIndex;
    actualToolName;
    constructor(opts) {
        super(`PT-BATCH-2: Tool-name spoofing detected. ` +
            `Batch '${opts.batchId}' declared tool '${opts.declaredToolName}' but item at index ${opts.divergedItemIndex} ` +
            `used tool '${opts.actualToolName}'. Batch invalidated for agent '${opts.agentId}'.`);
        this.name = 'BatchToolNameDivergedError';
        this.agentId = opts.agentId;
        this.batchId = opts.batchId;
        this.declaredToolName = opts.declaredToolName;
        this.divergedItemIndex = opts.divergedItemIndex;
        this.actualToolName = opts.actualToolName;
    }
}
export class WildcardToolNameError extends Error {
    agentId;
    constructor(agentId) {
        super(`Wildcard tool names ('*', '**', 'any', '') are rejected in batch allowlists ` +
            `for agent '${agentId}'. Operator must provide an explicit list of tool names.`);
        this.name = 'WildcardToolNameError';
        this.agentId = agentId;
    }
}
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Hard ceiling for batch size. Operators may configure lower but not higher. */
export const MAX_BATCH_SIZE_CEILING = 1_000;
/**
 * Threshold above which a LARGE_LOW_RISK_BATCH_DECLARED audit marker is
 * emitted (PT-BATCH-3 compensating control).
 */
export const LARGE_BATCH_AUDIT_THRESHOLD = 500;
/**
 * Pack IDs considered strict for batch-mode purposes.
 * Strict packs: low-risk only. Medium-risk batches are refused.
 */
export const STRICT_PACK_IDS = new Set([
    'hipaa',
    'hitech',
    'gdpr',
    'uk-gdpr',
    'pci-dss',
    'pcidss',
    'hmda',
    'dora',
]);
// ---------------------------------------------------------------------------
// BatchModeGovernor
// ---------------------------------------------------------------------------
export class BatchModeGovernor {
    /** key: `${orgId}::${agentId}` -> BatchModeConfig */
    configs = new Map();
    /** key: batchId -> ActiveBatch (cleared on recordBatchResult) */
    activeBatches = new Map();
    /** key: `${orgId}::${agentId}` -> DailyStats */
    dailyStats = new Map();
    /**
     * SHA-256 chain: the hash of the last emitted audit event per
     * `${orgId}::${agentId}`. Initialised to the zero hash.
     */
    auditChain = new Map();
    rateLimiter = null;
    tokenBudget = null;
    /** Callback for emitting audit events. Caller routes to AuditLogger / EventBus. */
    onAuditEvent = null;
    // ---------------------------------------------------------------------------
    // Dependency injection
    // ---------------------------------------------------------------------------
    /**
     * Wire the PR #14 rate limiter.
     * The batch governor calls hook() N times atomically before releasing items.
     */
    setRateLimiter(adapter) {
        this.rateLimiter = adapter;
    }
    /**
     * Wire the PR #12/#30 token budget enforcer.
     * After recordBatchResult the governor reports Sigma tokens once.
     */
    setTokenBudget(adapter) {
        this.tokenBudget = adapter;
    }
    /**
     * Register the audit event callback.
     * The callback receives fully-formed BatchAuditEvent objects with
     * prev-SHA chained. The caller routes them to the audit log.
     */
    setAuditCallback(callback) {
        this.onAuditEvent = callback;
    }
    // ---------------------------------------------------------------------------
    // configure()
    // ---------------------------------------------------------------------------
    /**
     * Operator-only configuration.
     *
     * @param orgId            Organisation / tenant identifier.
     * @param agentId          Agent being configured.
     * @param opts.maxBatchSize Ceiling: operator may set 1..(MAX_BATCH_SIZE_CEILING).
     * @param opts.allowedToolNames  Explicit tool names. Wildcards rejected.
     * @param opts.packBinding  Mandatory pack binding for this configuration.
     * @param approverIdentity  OperatorRoles.checkPermission result -- caller must
     *                          verify 'governance.configure' before calling this.
     */
    configure(orgId, agentId, opts, approverIdentity) {
        // Pack binding mandatory.
        if (!opts.packBinding || opts.packBinding.length === 0) {
            throw new EmptyPackBindingError(agentId);
        }
        // Wildcard check: reject any tool name that is a wildcard sentinel.
        const wildcardSentinels = new Set(['*', '**', 'any', '']);
        for (const toolName of opts.allowedToolNames) {
            if (wildcardSentinels.has(toolName.trim())) {
                throw new WildcardToolNameError(agentId);
            }
        }
        // Enforce the hard ceiling.
        if (opts.maxBatchSize > MAX_BATCH_SIZE_CEILING) {
            throw new Error(`Requested maxBatchSize ${opts.maxBatchSize} exceeds the hard ceiling of ` +
                `${MAX_BATCH_SIZE_CEILING}. Batch governance overhead amortisation breakeven ` +
                `is defined at ${MAX_BATCH_SIZE_CEILING} items.`);
        }
        if (opts.maxBatchSize < 1) {
            throw new Error(`maxBatchSize must be >= 1, got ${opts.maxBatchSize}.`);
        }
        const key = this.configKey(orgId, agentId);
        this.configs.set(key, {
            maxBatchSize: opts.maxBatchSize,
            allowedToolNames: [...opts.allowedToolNames],
            packBinding: [...opts.packBinding],
            configuredBy: approverIdentity,
            configuredAt: new Date().toISOString(),
        });
    }
    // ---------------------------------------------------------------------------
    // guardPreBatch()
    // ---------------------------------------------------------------------------
    /**
     * Pre-batch gate. Validates the batch envelope in one roundtrip.
     *
     * On success: calls the rate limiter N times atomically and returns a
     * { allowed: true, batchId } token the caller passes to recordBatchResult.
     *
     * On failure: throws one of the typed BatchMode errors.
     */
    guardPreBatch(orgId, agentId, cycleId, batch, packBinding) {
        // --- 1. Pack binding mandatory ---
        if (!packBinding || packBinding.length === 0) {
            throw new EmptyPackBindingError(agentId);
        }
        // --- 2. Configuration exists ---
        const key = this.configKey(orgId, agentId);
        const config = this.configs.get(key);
        if (!config) {
            throw new BatchNotConfiguredError(agentId);
        }
        // --- 3. High-risk: unconditionally forbidden ---
        if (batch.riskProfile === 'high') {
            throw new BatchHighRiskForbiddenError({ agentId, packBinding });
        }
        // --- 4. PT-BATCH-1: Homogeneity check -- reject per-item risk overrides ---
        for (let i = 0; i < batch.items.length; i++) {
            const item = batch.items[i];
            if (item.riskProfile !== undefined) {
                throw new BatchHeterogeneousRiskError({
                    agentId,
                    batchRiskProfile: batch.riskProfile,
                    itemIndex: i,
                });
            }
        }
        // --- 5. Tool name in allowlist ---
        if (!config.allowedToolNames.includes(batch.toolName)) {
            throw new ToolNotInBatchAllowlistError({
                agentId,
                toolName: batch.toolName,
                allowedToolNames: config.allowedToolNames,
                packBinding,
            });
        }
        // --- 6. Batch size ---
        if (batch.items.length > config.maxBatchSize) {
            throw new BatchSizeExceededError({
                agentId,
                requested: batch.items.length,
                limit: config.maxBatchSize,
                packBinding,
            });
        }
        // --- 7. Pack-tier risk gating ---
        if (batch.riskProfile === 'medium') {
            const restrictingPacks = packBinding.filter((p) => STRICT_PACK_IDS.has(p));
            if (restrictingPacks.length > 0) {
                throw new BatchModeRestrictedByPackError({
                    agentId,
                    riskProfile: 'medium',
                    restrictingPacks,
                });
            }
        }
        // --- 8. Rate limiter: call N times atomically ---
        if (this.rateLimiter) {
            for (let i = 0; i < batch.items.length; i++) {
                this.rateLimiter.hook(agentId, batch.toolName, packBinding);
            }
        }
        // --- 9. Register active batch ---
        const batchId = `batch_${crypto.randomBytes(8).toString('hex')}`;
        const active = {
            orgId,
            agentId,
            cycleId,
            toolName: batch.toolName,
            riskProfile: batch.riskProfile,
            itemCount: batch.items.length,
            packBinding: [...packBinding],
            startedAt: Date.now(),
            batchId,
        };
        this.activeBatches.set(batchId, active);
        // --- 10. PT-BATCH-3: Large low-risk batch marker ---
        if (batch.riskProfile === 'low' && batch.items.length >= LARGE_BATCH_AUDIT_THRESHOLD) {
            this.emitAuditEvent({
                type: 'governance:large-low-risk-batch-declared',
                severity: 'WARNING',
                orgId,
                agentId,
                cycleId,
                batchId,
                toolName: batch.toolName,
                itemCount: batch.items.length,
                riskProfile: batch.riskProfile,
                packBinding: [...packBinding],
                timestamp: new Date().toISOString(),
            });
        }
        return { allowed: true, batchId };
    }
    // ---------------------------------------------------------------------------
    // recordBatchResult()
    // ---------------------------------------------------------------------------
    /**
     * Post-batch gate. Emits one enriched audit event covering the batch.
     *
     * No raw item content enters the audit payload. Only aggregate statistics
     * (count, success, error, classification distribution) are captured.
     *
     * @param batchId   Token returned by guardPreBatch.
     * @param results   Per-item results (no raw content).
     * @param packBinding Mandatory. Should match what was passed to guardPreBatch.
     */
    recordBatchResult(orgId, agentId, cycleId, batchId, results, packBinding) {
        // --- 1. Pack binding mandatory ---
        if (!packBinding || packBinding.length === 0) {
            throw new EmptyPackBindingError(agentId);
        }
        // --- 2. Batch must be active ---
        const active = this.activeBatches.get(batchId);
        if (!active) {
            throw new BatchIdNotFoundError({ batchId, agentId });
        }
        const durationMs = Date.now() - active.startedAt;
        // --- 3. PT-BATCH-2: Tool-name spoofing check ---
        // Re-validate that all results used the declared toolName.
        let toolDivergenceIndex = -1;
        let divergedToolName = '';
        for (let i = 0; i < results.length; i++) {
            const result = results[i];
            if (result.toolName !== undefined && result.toolName !== active.toolName) {
                toolDivergenceIndex = i;
                divergedToolName = result.toolName;
                break;
            }
        }
        if (toolDivergenceIndex >= 0) {
            // Invalidate the batch.
            this.activeBatches.delete(batchId);
            this.emitAuditEvent({
                type: 'governance:batch-tool-name-diverged',
                severity: 'ERROR',
                orgId,
                agentId,
                cycleId,
                batchId,
                toolName: active.toolName,
                itemCount: active.itemCount,
                riskProfile: active.riskProfile,
                packBinding: [...packBinding],
                timestamp: new Date().toISOString(),
            });
            throw new BatchToolNameDivergedError({
                agentId,
                batchId,
                declaredToolName: active.toolName,
                divergedItemIndex: toolDivergenceIndex,
                actualToolName: divergedToolName,
            });
        }
        // --- 4. Build aggregate stats (no raw content) ---
        let successCount = 0;
        let errorCount = 0;
        const classificationDistribution = {};
        let totalTokens = 0;
        for (const result of results) {
            if (result.success) {
                successCount++;
            }
            else {
                errorCount++;
            }
            // Only count the classification label -- never the item content.
            const label = result.classification;
            classificationDistribution[label] = (classificationDistribution[label] ?? 0) + 1;
            if (result.tokensConsumed !== undefined) {
                totalTokens += result.tokensConsumed;
            }
        }
        // --- 5. Token budget: report Sigma tokens once post-batch ---
        if (this.tokenBudget && totalTokens > 0) {
            this.tokenBudget.recordUsage(agentId, cycleId, totalTokens, packBinding);
        }
        // --- 6. Update daily stats ---
        this.updateDailyStats(orgId, agentId, active.itemCount);
        // --- 7. Emit BATCH_PROCESSED audit event ---
        const severity = active.riskProfile === 'medium' ? 'WARNING' : 'NOTICE';
        this.emitAuditEvent({
            type: 'governance:batch-processed',
            severity,
            orgId,
            agentId,
            cycleId,
            batchId,
            toolName: active.toolName,
            itemCount: active.itemCount,
            riskProfile: active.riskProfile,
            successCount,
            errorCount,
            classificationDistribution,
            durationMs,
            packBinding: [...packBinding],
            timestamp: new Date().toISOString(),
        });
        // --- 8. Clear active batch ---
        this.activeBatches.delete(batchId);
    }
    // ---------------------------------------------------------------------------
    // abandonCycleBatches()
    // ---------------------------------------------------------------------------
    /**
     * Called when a cycle ends (e.g. cycle-timeout fires).
     * Any batches still open on the cycle are abandoned and an
     * INCOMPLETE_BATCH_AT_CYCLE_END event severity ERROR is emitted per batch.
     */
    abandonCycleBatches(orgId, agentId, cycleId) {
        for (const [batchId, active] of this.activeBatches) {
            if (active.orgId === orgId && active.agentId === agentId && active.cycleId === cycleId) {
                this.emitAuditEvent({
                    type: 'governance:batch-incomplete-at-cycle-end',
                    severity: 'ERROR',
                    orgId,
                    agentId,
                    cycleId,
                    batchId,
                    toolName: active.toolName,
                    itemCount: active.itemCount,
                    riskProfile: active.riskProfile,
                    packBinding: [...active.packBinding],
                    timestamp: new Date().toISOString(),
                });
                this.activeBatches.delete(batchId);
            }
        }
    }
    // ---------------------------------------------------------------------------
    // getBatchStats()
    // ---------------------------------------------------------------------------
    /**
     * Returns daily statistics for an (orgId, agentId) scope.
     * Statistics reset at midnight UTC each day.
     */
    getBatchStats(orgId, agentId) {
        const key = this.configKey(orgId, agentId);
        const stats = this.dailyStats.get(key);
        const today = this.todayKey();
        if (!stats || stats.dateKey !== today) {
            return { batchesToday: 0, itemsToday: 0, avgBatchSize: 0, lastBatchAt: null };
        }
        const avgBatchSize = stats.batchesToday > 0
            ? Math.round(stats.itemsToday / stats.batchesToday)
            : 0;
        return {
            batchesToday: stats.batchesToday,
            itemsToday: stats.itemsToday,
            avgBatchSize,
            lastBatchAt: stats.lastBatchAt,
        };
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    configKey(orgId, agentId) {
        return `${orgId}::${agentId}`;
    }
    todayKey() {
        return new Date().toISOString().slice(0, 10);
    }
    updateDailyStats(orgId, agentId, itemCount) {
        const key = this.configKey(orgId, agentId);
        const today = this.todayKey();
        let stats = this.dailyStats.get(key);
        if (!stats || stats.dateKey !== today) {
            stats = { batchesToday: 0, itemsToday: 0, lastBatchAt: null, dateKey: today };
        }
        stats.batchesToday += 1;
        stats.itemsToday += itemCount;
        stats.lastBatchAt = new Date().toISOString();
        this.dailyStats.set(key, stats);
    }
    /**
     * Emit an audit event with prev-SHA chaining.
     * The chain is keyed per (orgId, agentId) -- one chain per agent.
     * Item content is NEVER included in any audit payload.
     */
    emitAuditEvent(event) {
        const chainKey = this.configKey(event.orgId, event.agentId);
        const prevSha = this.auditChain.get(chainKey) ?? '0'.repeat(64);
        const fullEvent = { ...event, prevSha };
        // Update chain with SHA-256 of current event.
        const eventJson = JSON.stringify(fullEvent);
        const newSha = crypto.createHash('sha256').update(eventJson, 'utf8').digest('hex');
        this.auditChain.set(chainKey, newSha);
        this.onAuditEvent?.(fullEvent);
    }
    /**
     * Expose the current chain hash for a given (orgId, agentId).
     * Useful for tests and audit-integrity verification.
     */
    getChainHash(orgId, agentId) {
        return this.auditChain.get(this.configKey(orgId, agentId)) ?? '0'.repeat(64);
    }
    /**
     * Check whether a given (orgId, agentId) scope has been configured.
     */
    isConfigured(orgId, agentId) {
        return this.configs.has(this.configKey(orgId, agentId));
    }
}
//# sourceMappingURL=batch-mode-governance.js.map