/**
 * AuditNarrativeGenerator -- P4-AIR-01 + P4-AIR-03
 *
 * Generates AI-authored narrative sections for auditor-facing compliance
 * reports. Pack-binding primacy is enforced at two levels:
 *
 *   1. AuditChainReader.getEventsInWindow() is called with the frameworks
 *      filter so the storage layer only returns bound events.
 *   2. A second-pass filter in this module drops any event whose packBinding
 *      does not intersect with req.frameworks, guarding against storage
 *      implementations that ignore the filter.
 *
 * Every AI-authored section is signed with an AiProvenance record and sealed
 * into the tamper-evident audit chain via sealProvenanceToChain() BEFORE the
 * NarrativeReport is returned. Callers cannot receive a report without
 * provenance being sealed (fail-closed).
 *
 * Spec: P4-AIR-01 AC 1-3, P4-AIR-03 AC 7-9
 *
 * @connexum/ai-governance
 */
import * as crypto from 'node:crypto';
import { UncitedClaimError, MissingProvenanceError, ChainTamperDetectedError, } from './narrative-types.js';
import { PROMPT_TEMPLATE_IDS, getTemplateStaticText, buildAgentToolUsePrompt, buildApprovalDecisionPrompt, buildPolicyViolationPrompt, buildEnforcementEventPrompt, } from './report-templates/prompts/base.js';
// ---------------------------------------------------------------------------
// Internal constants
// ---------------------------------------------------------------------------
const SDK_VERSION = '1.0.0-beta.1';
const DEFAULT_TEMPERATURE = 0.2; // Low temperature for deterministic compliance prose
// ---------------------------------------------------------------------------
// Event-to-citation pattern (extracts [event-id] refs from prose)
// ---------------------------------------------------------------------------
// Matches [some-event-id] but not [ai:sectionId] watermarks
const CITATION_PATTERN = /\[(?!ai:)([^\]]+)\]/g;
/**
 * Extract all event IDs cited in prose via [event-id] syntax.
 */
function extractCitedIds(prose) {
    const ids = [];
    let match;
    CITATION_PATTERN.lastIndex = 0;
    while ((match = CITATION_PATTERN.exec(prose)) !== null) {
        ids.push(match[1]);
    }
    return ids;
}
/**
 * Validate that every ID cited in prose exists in the allowed event set.
 * Throws UncitedClaimError (fail-closed) if any invented IDs are found.
 */
function validateCitations(prose, allowedIds, anchor) {
    const cited = extractCitedIds(prose);
    const invented = cited.filter((id) => !allowedIds.has(id));
    if (invented.length > 0) {
        throw new UncitedClaimError(invented, anchor);
    }
}
// ---------------------------------------------------------------------------
// SHA-256 helpers
// ---------------------------------------------------------------------------
function sha256(input) {
    return crypto.createHash('sha256').update(input, 'utf8').digest('hex');
}
function sha256Events(events) {
    return sha256(JSON.stringify(events));
}
function computeSectionSignature(sectionId, promptTemplateHash, inputChunkHashes) {
    return sha256(sectionId + promptTemplateHash + inputChunkHashes.join(','));
}
// ---------------------------------------------------------------------------
// Event category classification
// ---------------------------------------------------------------------------
const TOOL_USE_TYPES = new Set([
    'action:allowed',
    'action:denied',
    'action:approval_required',
    'agent:tool-use',
    'tool:called',
    'tool:blocked',
]);
const APPROVAL_TYPES = new Set([
    'approval:requested',
    'approval:granted',
    'approval:denied',
    'approval:expired',
    'approval:decided',
]);
const POLICY_VIOLATION_TYPES = new Set([
    'policy:violated',
    'policy:violation',
    'agent:suspended',
    'governance:tampered',
    'memory:tampered',
]);
const ENFORCEMENT_TYPES = new Set([
    'circuit:opened',
    'circuit:closed',
    'anomaly:detected',
    'incident:created',
    'incident:resolved',
    'agent:suspended',
    'agent:resumed',
]);
function categoriseEvent(event) {
    if (TOOL_USE_TYPES.has(event.type))
        return 'agent-tool-use';
    if (APPROVAL_TYPES.has(event.type))
        return 'approval-decision';
    if (POLICY_VIOLATION_TYPES.has(event.type))
        return 'policy-violation';
    if (ENFORCEMENT_TYPES.has(event.type))
        return 'enforcement-event';
    return null;
}
export class AuditNarrativeGenerator {
    llmAdapter;
    auditChainReader;
    packs;
    auditIntegrity;
    auditChainWriter;
    constructor(config) {
        this.llmAdapter = config.llmAdapter;
        this.auditChainReader = config.auditChainReader;
        this.packs = config.packs;
        this.auditIntegrity = config.auditIntegrity;
        this.auditChainWriter = config.auditChainWriter;
    }
    // -------------------------------------------------------------------------
    // Public: generate
    // -------------------------------------------------------------------------
    async generate(req) {
        const reportId = crypto.randomUUID();
        const generationStart = new Date().toISOString();
        // Step 0 -- P1-TAMPER-C: pre-report chain attestation gate.
        // A report generated from a tampered chain is a false attestation.
        // Fail-closed: verify BEFORE reading any events.
        if (this.auditIntegrity !== undefined) {
            const chainCheck = this.auditIntegrity.verify();
            if (!chainCheck.valid) {
                // Append a forensic chain.report.refused event before throwing
                if (this.auditChainWriter !== undefined) {
                    try {
                        const refusedId = crypto.randomUUID();
                        const latestHash = (await this.auditChainWriter.getLatestChainHash(req.tenantId)) ?? '';
                        const refusedData = {
                            eventType: 'chain.report.refused',
                            tenantId: req.tenantId,
                            reportId,
                            brokenAt: chainCheck.brokenAt,
                            errors: chainCheck.errors.slice(0, 3), // cap to avoid large payloads
                            refusedAt: new Date().toISOString(),
                        };
                        const refusedHash = sha256(JSON.stringify({ id: refusedId, prevHash: latestHash, data: refusedData }));
                        await this.auditChainWriter.appendAuditChain({
                            id: refusedId,
                            orgId: req.tenantId,
                            previousHash: latestHash,
                            currentHash: refusedHash,
                            entryData: refusedData,
                            timestamp: new Date().toISOString(),
                        });
                    }
                    catch {
                        // Never let chain-write failure swallow the original error
                    }
                }
                // Extract detail from the first error message
                const firstError = chainCheck.errors[0] ?? '';
                const expectedMatch = firstError.match(/Expected ([0-9a-f]+)/);
                const observedMatch = firstError.match(/got ([0-9a-f]+)/);
                throw new ChainTamperDetectedError({
                    firstBadEventId: `seq-${chainCheck.brokenAt}`,
                    expectedHash: expectedMatch?.[1] ?? '',
                    observedHash: observedMatch?.[1] ?? '',
                    brokenAt: chainCheck.brokenAt,
                });
            }
        }
        // Step 1 -- read events from the audit chain, pre-filtered by storage layer
        const rawEvents = await this.auditChainReader.getEventsInWindow({
            tenantId: req.tenantId,
            windowStart: req.windowStart,
            windowEnd: req.windowEnd,
            frameworks: req.frameworks,
        });
        // Step 2 -- second-pass pack-binding filter (defense-in-depth against
        // storage implementations that ignore the frameworks filter)
        const boundEvents = rawEvents.filter((e) => e.packBinding.some((packId) => req.frameworks.includes(packId)));
        const totalEventsRead = boundEvents.length;
        // Step 3 -- generate one section per (framework x eventCategory) pair
        const sections = [];
        const allInputChunkHashes = [];
        const allCitedEventIds = new Set();
        let totalTokensUsed = 0;
        for (const framework of req.frameworks) {
            const frameworkEvents = boundEvents.filter((e) => e.packBinding.includes(framework));
            const categories = [
                { key: 'agent-tool-use', templateId: PROMPT_TEMPLATE_IDS.AGENT_TOOL_USE },
                { key: 'approval-decision', templateId: PROMPT_TEMPLATE_IDS.APPROVAL_DECISION },
                { key: 'policy-violation', templateId: PROMPT_TEMPLATE_IDS.POLICY_VIOLATION },
                { key: 'enforcement-event', templateId: PROMPT_TEMPLATE_IDS.ENFORCEMENT_EVENT },
            ];
            for (const { key, templateId } of categories) {
                const categoryEvents = frameworkEvents.filter((e) => categoriseEvent(e) === key);
                // Compute input chunk hash before LLM call
                const chunkHash = sha256Events(categoryEvents);
                allInputChunkHashes.push(chunkHash);
                // Build the prompt
                const promptParams = {
                    framework,
                    audience: req.audience,
                    events: categoryEvents,
                    windowStart: req.windowStart,
                    windowEnd: req.windowEnd,
                };
                let promptResult;
                switch (key) {
                    case 'agent-tool-use':
                        promptResult = buildAgentToolUsePrompt(promptParams);
                        break;
                    case 'approval-decision':
                        promptResult = buildApprovalDecisionPrompt(promptParams);
                        break;
                    case 'policy-violation':
                        promptResult = buildPolicyViolationPrompt(promptParams);
                        break;
                    case 'enforcement-event':
                        promptResult = buildEnforcementEventPrompt(promptParams);
                        break;
                }
                // Call the LLM
                const llmResponse = await this.llmAdapter.chat({
                    modelId: this.llmAdapter.adapterId,
                    messages: [{ role: 'user', content: promptResult.user }],
                    systemPrompt: promptResult.system,
                    temperature: DEFAULT_TEMPERATURE,
                    maxTokens: 1024,
                });
                totalTokensUsed += llmResponse.usage?.totalTokens ?? 0;
                const prose = llmResponse.content;
                // Build the allowed ID set for citation validation
                const allowedIds = new Set(categoryEvents.map((e) => e.id));
                const anchor = `${framework}-${key}-${reportId.slice(0, 8)}`;
                // Validate citations (fail-closed)
                validateCitations(prose, allowedIds, anchor);
                // Build cited event refs from the prose citations
                const citedIds = extractCitedIds(prose);
                const citedEvents = citedIds
                    .filter((id) => allowedIds.has(id))
                    .map((id) => {
                    const event = categoryEvents.find((e) => e.id === id);
                    return {
                        eventId: event.id,
                        eventType: event.type,
                        eventTimestamp: event.timestamp,
                        summary: event.reason,
                    };
                });
                for (const ref of citedEvents) {
                    allCitedEventIds.add(ref.eventId);
                }
                sections.push({
                    framework,
                    anchor,
                    prose,
                    citedEvents,
                    citedReviewers: [],
                    promptTemplateId: templateId,
                });
            }
        }
        // Step 4 -- build provenance (not yet signed / sealed)
        const promptTemplateHash = sha256(Object.values(PROMPT_TEMPLATE_IDS)
            .map((id) => getTemplateStaticText(id))
            .join('\n'));
        const sectionSignatures = sections.map((s) => ({
            sectionId: s.anchor,
            signatureHash: computeSectionSignature(s.anchor, promptTemplateHash, allInputChunkHashes),
        }));
        const provenance = {
            modelProvider: this.llmAdapter.providerId,
            modelId: this.llmAdapter.adapterId,
            modelVersion: 'runtime-resolved',
            promptTemplateHash,
            inputChunkHashes: allInputChunkHashes,
            temperature: DEFAULT_TEMPERATURE,
            generatedAt: new Date().toISOString(),
            sdkVersion: SDK_VERSION,
            sectionSignatures,
        };
        const report = {
            reportId,
            tenantId: req.tenantId,
            contractId: req.contractId,
            window: { start: req.windowStart, end: req.windowEnd },
            frameworks: req.frameworks,
            audience: req.audience,
            sections,
            provenance,
            metadata: {
                totalEventsRead,
                totalEventsCited: allCitedEventIds.size,
                tokensUsed: totalTokensUsed,
                generatedAt: generationStart,
            },
        };
        return report;
    }
}
// ---------------------------------------------------------------------------
// sealProvenanceToChain (P4-AIR-03 AC 7, fail-closed)
// ---------------------------------------------------------------------------
/**
 * Emit one report:ai-section-generated audit-chain event per NarrativeSection,
 * carrying the section's signature hash so auditors can detect tampering.
 *
 * This function is fail-closed: if the chain append fails for any section,
 * the error is thrown immediately and the report is NOT returned to the caller.
 *
 * After sealing, each SectionSignature.chainAnchorRef is populated with the
 * ID of the chain entry that sealed it.
 */
export async function sealProvenanceToChain(report, provenance, chainWriter) {
    const latestHash = (await chainWriter.getLatestChainHash(report.tenantId)) ?? '';
    let prevHash = latestHash;
    for (let i = 0; i < provenance.sectionSignatures.length; i++) {
        const sig = provenance.sectionSignatures[i];
        const section = report.sections[i];
        const entryId = crypto.randomUUID();
        const entryData = {
            eventType: 'report:ai-section-generated',
            reportId: report.reportId,
            tenantId: report.tenantId,
            sectionAnchor: sig.sectionId,
            framework: section?.framework ?? 'unknown',
            signatureHash: sig.signatureHash,
            promptTemplateHash: provenance.promptTemplateHash,
            modelProvider: provenance.modelProvider,
            modelId: provenance.modelId,
            temperature: provenance.temperature,
            generatedAt: provenance.generatedAt,
            inputChunkHashes: provenance.inputChunkHashes,
        };
        const currentHash = sha256(JSON.stringify({ id: entryId, prevHash, data: entryData }));
        // Fail-closed: throw if append fails
        await chainWriter.appendAuditChain({
            id: entryId,
            orgId: report.tenantId,
            previousHash: prevHash,
            currentHash,
            entryData,
            timestamp: new Date().toISOString(),
        });
        // Backfill chainAnchorRef on the signature
        sig.chainAnchorRef = {
            eventId: entryId,
            eventType: 'report:ai-section-generated',
            eventTimestamp: new Date().toISOString(),
            summary: `Sealed section "${sig.sectionId}" signature ${sig.signatureHash.slice(0, 16)}...`,
        };
        prevHash = currentHash;
    }
}
// ---------------------------------------------------------------------------
// renderNarrativeReport (P4-AIR-03 AC 8-9)
// ---------------------------------------------------------------------------
/**
 * Render a NarrativeReport to markdown or HTML.
 *
 * Enforces:
 *   - Provenance must be present (MissingProvenanceError if absent)
 *   - AI-authored disclosure block at the top (EU AI Act Art. 50(1))
 *   - Per-section watermark at the end of each section (AC 9)
 *     Markdown: <!--[ai:sectionId]-->
 *     HTML:     <span class="ai-mark" aria-hidden="true">[ai:sectionId]</span>
 */
export function renderNarrativeReport(report, format) {
    if (!report.provenance) {
        throw new MissingProvenanceError(report.reportId);
    }
    const prov = report.provenance;
    if (format === 'markdown') {
        return renderMarkdown(report, prov);
    }
    return renderHtml(report, prov);
}
// ---------------------------------------------------------------------------
// Markdown renderer
// ---------------------------------------------------------------------------
function renderMarkdown(report, prov) {
    const chainRef = prov.sectionSignatures[0]?.chainAnchorRef?.eventId ?? 'pending';
    let md = '';
    // Disclosure block (AC 8 -- operator cannot skip this)
    md += `> **AI-Generated Report Disclosure**\n`;
    md += `> Sections of this report were generated by ${prov.modelProvider} model \`${prov.modelId}\`.\n`;
    md += `> Provenance detail at chain reference: \`${chainRef}\`.\n`;
    md += `> Generated: ${prov.generatedAt} | SDK: ${prov.sdkVersion}\n\n`;
    md += `# Compliance Audit Narrative Report\n\n`;
    md += `**Report ID:** ${report.reportId}\n`;
    md += `**Tenant:** ${report.tenantId}\n`;
    md += `**Contract:** ${report.contractId}\n`;
    md += `**Window:** ${report.window.start} to ${report.window.end}\n`;
    md += `**Frameworks:** ${report.frameworks.join(', ')}\n`;
    md += `**Audience:** ${report.audience}\n`;
    md += `**Generated:** ${report.metadata.generatedAt}\n\n`;
    md += `## Report Metadata\n\n`;
    md += `- Events read: ${report.metadata.totalEventsRead}\n`;
    md += `- Events cited: ${report.metadata.totalEventsCited}\n`;
    md += `- Tokens used: ${report.metadata.tokensUsed}\n\n`;
    for (const section of report.sections) {
        md += `## ${formatSectionTitle(section.anchor)} (${section.framework})\n\n`;
        md += section.prose + '\n\n';
        if (section.citedEvents.length > 0) {
            md += `**Cited Events:**\n`;
            for (const ref of section.citedEvents) {
                md += `- \`${ref.eventId}\` -- ${ref.eventType} (${ref.eventTimestamp})\n`;
            }
            md += '\n';
        }
        // Per-section watermark (AC 9) -- invisible to readers, searchable in source
        md += `<!--[ai:${section.anchor}]-->\n\n`;
    }
    return md;
}
// ---------------------------------------------------------------------------
// HTML renderer
// ---------------------------------------------------------------------------
function renderHtml(report, prov) {
    const chainRef = prov.sectionSignatures[0]?.chainAnchorRef?.eventId ?? 'pending';
    const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
    let html = '<!DOCTYPE html>\n<html lang="en">\n<head>\n';
    html += '<meta charset="UTF-8">\n';
    html += `<title>Compliance Audit Narrative Report</title>\n`;
    html += '<style>\n';
    html += 'body{font-family:-apple-system,sans-serif;max-width:960px;margin:0 auto;padding:20px;color:#333}\n';
    html += '.disclosure{background:#fffbe6;border-left:4px solid #f0ad4e;padding:12px 16px;margin-bottom:24px}\n';
    html += '.ai-mark{display:none}\n';
    html += 'h1,h2{color:#1a1a1a}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:8px}\n';
    html += '</style>\n</head>\n<body>\n';
    // Disclosure block (AC 8)
    html += `<div class="disclosure" role="note">\n`;
    html += `<strong>AI-Generated Report Disclosure</strong><br>\n`;
    html += `Sections of this report were generated by ${esc(prov.modelProvider)} model <code>${esc(prov.modelId)}</code>.<br>\n`;
    html += `Provenance detail at chain reference: <code>${esc(chainRef)}</code>.<br>\n`;
    html += `Generated: ${esc(prov.generatedAt)} | SDK: ${esc(prov.sdkVersion)}\n`;
    html += `</div>\n\n`;
    html += `<h1>Compliance Audit Narrative Report</h1>\n`;
    html += `<p><strong>Report ID:</strong> ${esc(report.reportId)}<br>\n`;
    html += `<strong>Tenant:</strong> ${esc(report.tenantId)}<br>\n`;
    html += `<strong>Contract:</strong> ${esc(report.contractId)}<br>\n`;
    html += `<strong>Window:</strong> ${esc(report.window.start)} to ${esc(report.window.end)}<br>\n`;
    html += `<strong>Frameworks:</strong> ${esc(report.frameworks.join(', '))}<br>\n`;
    html += `<strong>Audience:</strong> ${esc(report.audience)}</p>\n\n`;
    for (const section of report.sections) {
        html += `<h2>${esc(formatSectionTitle(section.anchor))} (${esc(section.framework)})</h2>\n`;
        html += `<p>${esc(section.prose).replace(/\n/g, '<br>')}</p>\n`;
        if (section.citedEvents.length > 0) {
            html += `<p><strong>Cited Events:</strong></p>\n<ul>\n`;
            for (const ref of section.citedEvents) {
                html += `<li><code>${esc(ref.eventId)}</code> -- ${esc(ref.eventType)} (${esc(ref.eventTimestamp)})</li>\n`;
            }
            html += `</ul>\n`;
        }
        // Per-section watermark (AC 9)
        html += `<span class="ai-mark" aria-hidden="true">[ai:${esc(section.anchor)}]</span>\n\n`;
    }
    html += '</body>\n</html>';
    return html;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function formatSectionTitle(anchor) {
    // anchor format: <framework>-<category>-<shortId>
    const parts = anchor.split('-');
    if (parts.length < 2)
        return anchor;
    const category = parts.slice(1, -1).join(' ');
    return category
        .split('-')
        .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
        .join(' ');
}
// ---------------------------------------------------------------------------
// Provenance signature validator (for PT-AIR-3)
// ---------------------------------------------------------------------------
/**
 * Verify that a sectionSignature stored in the audit chain matches the
 * actual prompt template hash. Catches forged chain entries where an
 * attacker swapped the promptTemplateHash to a different value.
 *
 * Returns true if valid, false if tampered.
 */
export function verifyProvenanceSignature(sectionId, claimedSignatureHash, claimedPromptTemplateHash, inputChunkHashes) {
    const expected = computeSectionSignature(sectionId, claimedPromptTemplateHash, inputChunkHashes);
    return expected === claimedSignatureHash;
}
//# sourceMappingURL=narrative-generator.js.map