/**
 * GovernanceReporter -- SDK Event Reporter (Phase 1C).
 *
 * Streams governance events to the My Compliance Center SaaS API.
 * Supports batching, WAL-based durability, heartbeats, and dry-run mode.
 *
 * Uses Node built-in fetch (Node 18+) and crypto.randomUUID().
 * No external HTTP dependencies.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
import { createLogger } from '../util/log.js';
import { PluginRegistry } from '../governance/plugin-system.js';
const log = createLogger('governance-reporter');
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const DEFAULT_BATCH_SIZE = 50;
const DEFAULT_FLUSH_INTERVAL_MS = 10_000;
const DEFAULT_HEARTBEAT_INTERVAL_MS = 60_000;
const DEFAULT_MAX_WAL_SIZE = 1000;
const WAL_FILENAME = 'governance-wal.jsonl';
// ---------------------------------------------------------------------------
// GovernanceReporter
// ---------------------------------------------------------------------------
export class GovernanceReporter {
    config;
    batchSize;
    flushIntervalMs;
    heartbeatIntervalMs;
    maxWalSize;
    walPath;
    dryRun;
    queue = [];
    flushTimer = null;
    heartbeatTimer = null;
    // SYSTEMIC-L1: counter for malformed WAL entries. Even a single malformed
    // entry is a tampering signal under 21 CFR Part 11 and SOC 2 CC7.2.
    malformedWalCount = 0;
    // SYSTEMIC-H4-WIRE: optional plugin registry for report wiring.
    pluginRegistry;
    constructor(config) {
        // Validate required fields
        if (!config.apiEndpoint) {
            throw new Error('GovernanceReporterConfig.apiEndpoint is required');
        }
        if (!config.licenseKey) {
            throw new Error('GovernanceReporterConfig.licenseKey is required');
        }
        if (!config.installationId) {
            throw new Error('GovernanceReporterConfig.installationId is required');
        }
        if (!config.orgId) {
            throw new Error('GovernanceReporterConfig.orgId is required');
        }
        this.config = config;
        this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
        this.flushIntervalMs = config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
        this.heartbeatIntervalMs = config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
        this.maxWalSize = config.maxWalSize ?? DEFAULT_MAX_WAL_SIZE;
        this.dryRun = config.dryRun ?? false;
        this.pluginRegistry = config.pluginRegistry;
        // WAL setup
        if (config.walDir) {
            if (!fs.existsSync(config.walDir)) {
                fs.mkdirSync(config.walDir, { recursive: true });
            }
            this.walPath = path.join(config.walDir, WAL_FILENAME);
            this.loadWal();
        }
        else {
            this.walPath = null;
        }
        // Start timers
        this.flushTimer = setInterval(() => {
            this.flush().catch((err) => {
                log.error('Periodic flush failed', { error: String(err) });
            });
        }, this.flushIntervalMs);
        this.heartbeatTimer = setInterval(() => {
            this.sendHeartbeat().catch((err) => {
                log.error('Heartbeat failed', { error: String(err) });
            });
        }, this.heartbeatIntervalMs);
        // Unref timers so they don't prevent process exit
        if (this.flushTimer && typeof this.flushTimer.unref === 'function') {
            this.flushTimer.unref();
        }
        if (this.heartbeatTimer && typeof this.heartbeatTimer.unref === 'function') {
            this.heartbeatTimer.unref();
        }
        log.info('GovernanceReporter initialized', {
            apiEndpoint: config.apiEndpoint,
            installationId: config.installationId,
            dryRun: this.dryRun,
            walEnabled: !!this.walPath,
        });
    }
    // -------------------------------------------------------------------------
    // Public API
    // -------------------------------------------------------------------------
    /**
     * Add an event to the queue.
     * Triggers auto-flush when queue reaches batchSize.
     */
    reportEvent(event) {
        // P4-RET-01: Stamp DataClassTag at the governance-reporter WAL write site.
        // Fail-closed: if a classifier is configured, classification must succeed.
        if (this.config.dataClassifier && !event.dataClassTag) {
            event.dataClassTag = this.config.dataClassifier.classify('governance-reporter:wal:event', {
                tenantId: this.config.tenantId ?? '',
                contractId: this.config.contractId ?? '',
                activePackIds: this.config.activePackIds ?? [],
            });
        }
        this.queue.push(event);
        if (this.walPath) {
            this.appendToWal(event);
        }
        log.debug('Event queued', { type: event.type, queueDepth: this.queue.length });
        if (this.queue.length >= this.batchSize) {
            this.flush().catch((err) => {
                log.error('Auto-flush failed', { error: String(err) });
            });
        }
    }
    /**
     * Convenience: create a governance_decision event from adapter types.
     */
    reportDecision(context, decision) {
        const event = {
            id: crypto.randomUUID(),
            type: 'governance_decision',
            timestamp: new Date().toISOString(),
            installationId: this.config.installationId,
            orgId: this.config.orgId,
            payload: {
                toolName: context.toolName,
                agentName: context.agent.name,
                agentRole: context.agent.role,
                sessionId: context.agent.sessionId,
                decisionType: decision.type,
                reason: decision.reason,
                metadata: decision.metadata,
                input: context.input,
                actionTimestamp: context.timestamp,
            },
        };
        this.reportEvent(event);
    }
    /**
     * Flush queued events to the API.
     * On success, clears the WAL. On failure, events remain for retry.
     */
    async flush() {
        if (this.queue.length === 0) {
            return { sent: 0, failed: 0, walPending: this.getWalSize() };
        }
        const batch = this.queue.splice(0);
        if (this.dryRun) {
            log.debug('Dry-run flush', { count: batch.length });
            this.truncateWal();
            return { sent: batch.length, failed: 0, walPending: 0 };
        }
        try {
            const url = `${this.config.apiEndpoint}/v1/events/batch`;
            const response = await fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.config.licenseKey}`,
                    'X-Installation-Id': this.config.installationId,
                    'X-Org-Id': this.config.orgId,
                },
                body: JSON.stringify({ events: batch }),
            });
            if (response.ok) {
                log.info('Flush successful', { sent: batch.length });
                this.truncateWal();
                return { sent: batch.length, failed: 0, walPending: 0 };
            }
            // Non-OK response: put events back for retry
            log.warn('Flush failed, re-queuing', {
                status: response.status,
                count: batch.length,
            });
            this.queue.unshift(...batch);
            return { sent: 0, failed: batch.length, walPending: this.getWalSize() };
        }
        catch (err) {
            // Network error: put events back for retry
            log.error('Flush network error, re-queuing', {
                error: String(err),
                count: batch.length,
            });
            this.queue.unshift(...batch);
            return { sent: 0, failed: batch.length, walPending: this.getWalSize() };
        }
    }
    /**
     * Send a heartbeat to the API.
     */
    async sendHeartbeat() {
        if (this.dryRun) {
            log.debug('Dry-run heartbeat');
            return;
        }
        try {
            const url = `${this.config.apiEndpoint}/v1/installations/heartbeat`;
            await fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.config.licenseKey}`,
                    'X-Installation-Id': this.config.installationId,
                    'X-Org-Id': this.config.orgId,
                },
                body: JSON.stringify({
                    installationId: this.config.installationId,
                    timestamp: new Date().toISOString(),
                    queueDepth: this.queue.length,
                }),
            });
        }
        catch (err) {
            log.warn('Heartbeat request failed', { error: String(err) });
        }
    }
    /**
     * Graceful shutdown: clear timers and flush remaining events.
     */
    async shutdown() {
        if (this.flushTimer) {
            clearInterval(this.flushTimer);
            this.flushTimer = null;
        }
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
            this.heartbeatTimer = null;
        }
        log.info('Shutting down, flushing remaining events', {
            queueDepth: this.queue.length,
        });
        return this.flush();
    }
    /**
     * Render a governance report, including plugin-authored sections.
     *
     * SYSTEMIC-H4-WIRE: this is the authoritative report-render entry point.
     * It calls `PluginRegistry.executeOnReport()` (when a registry is wired)
     * to collect plugin sections, then calls
     * `PluginRegistry.summarizeReportPluginStatuses()` to build the banner
     * and incomplete-plugin metadata.
     *
     * IMPORTANT: callers that produce a human-readable or machine-readable
     * document from this output MUST render `banner` at the TOP of the
     * document before any other content. A non-null banner signals that the
     * report is incomplete and auditors must not treat it as definitive.
     *
     * @param context Caller-supplied context forwarded to plugin hooks.
     *   Include session identifiers, agent names, and policy pack IDs.
     * @param reportContext Additional metadata to embed in the report.
     * @returns RenderedReport with banner, plugin sections, and status summary.
     */
    renderReport(context, reportContext = {}) {
        const sections = [];
        if (this.pluginRegistry) {
            const pluginResults = this.pluginRegistry.executeOnReport(context);
            sections.push(...pluginResults);
        }
        const summary = PluginRegistry.summarizeReportPluginStatuses(sections);
        const report = {
            banner: summary.banner,
            generatedAt: new Date().toISOString(),
            installationId: this.config.installationId,
            orgId: this.config.orgId,
            pluginStatuses: summary.pluginStatuses,
            incompletePluginCount: summary.incompletePluginCount,
            incompletePlugins: summary.incompletePlugins,
            sections,
            context: reportContext,
        };
        log.info('Report rendered', {
            installationId: this.config.installationId,
            sectionCount: sections.length,
            incompletePluginCount: summary.incompletePluginCount,
            hasBanner: summary.banner !== null,
        });
        return report;
    }
    /**
     * Current number of events waiting in the queue.
     */
    getQueueDepth() {
        return this.queue.length;
    }
    /**
     * SYSTEMIC-L1: Number of malformed WAL entries encountered since startup.
     * A non-zero value is a tampering signal and should trigger investigation.
     * Exposed for operator tooling and test assertion.
     */
    getMalformedWalCount() {
        return this.malformedWalCount;
    }
    /**
     * Number of entries in the WAL file. Returns 0 if WAL is not configured.
     */
    getWalSize() {
        if (!this.walPath)
            return 0;
        if (!fs.existsSync(this.walPath))
            return 0;
        try {
            const content = fs.readFileSync(this.walPath, 'utf-8').trim();
            if (!content)
                return 0;
            return content.split('\n').length;
        }
        catch {
            return 0;
        }
    }
    // -------------------------------------------------------------------------
    // WAL internals
    // -------------------------------------------------------------------------
    loadWal() {
        if (!this.walPath || !fs.existsSync(this.walPath))
            return;
        try {
            const content = fs.readFileSync(this.walPath, 'utf-8').trim();
            if (!content)
                return;
            const lines = content.split('\n');
            for (const line of lines) {
                try {
                    const event = JSON.parse(line);
                    this.queue.push(event);
                }
                catch {
                    // SYSTEMIC-L1: increment tamper counter and emit audit event.
                    // Threshold is 1 -- any malformed entry is a SOC 2 / Part 11 signal.
                    this.malformedWalCount++;
                    log.warn('Skipping malformed WAL entry', {
                        malformedWalCount: this.malformedWalCount,
                    });
                    // Emit a structured audit event so downstream alerting can react.
                    // We enqueue a synthetic event rather than calling reportEvent()
                    // to avoid recursive WAL writes during load.
                    const auditEvent = {
                        id: crypto.randomUUID(),
                        type: 'compliance_score',
                        timestamp: new Date().toISOString(),
                        installationId: this.config.installationId,
                        orgId: this.config.orgId,
                        payload: {
                            eventType: 'reporter:wal-malformed-threshold',
                            malformedWalCount: this.malformedWalCount,
                            severity: 'HIGH',
                            detail: 'Malformed WAL entry detected. Possible tampering with audit trail. Investigate immediately.',
                        },
                    };
                    this.queue.push(auditEvent);
                }
            }
            log.info('Loaded events from WAL', { count: this.queue.length });
        }
        catch (err) {
            log.error('Failed to load WAL', { error: String(err) });
        }
    }
    appendToWal(event) {
        if (!this.walPath)
            return;
        try {
            fs.appendFileSync(this.walPath, JSON.stringify(event) + '\n');
            // Force flush if WAL exceeds max size
            if (this.getWalSize() >= this.maxWalSize) {
                log.warn('WAL size exceeded maxWalSize, forcing flush', {
                    walSize: this.getWalSize(),
                    maxWalSize: this.maxWalSize,
                });
                this.flush().catch((err) => {
                    log.error('WAL-triggered flush failed', { error: String(err) });
                });
            }
        }
        catch (err) {
            log.error('Failed to append to WAL', { error: String(err) });
        }
    }
    truncateWal() {
        if (!this.walPath)
            return;
        try {
            fs.writeFileSync(this.walPath, '');
        }
        catch (err) {
            log.error('Failed to truncate WAL', { error: String(err) });
        }
    }
}
//# sourceMappingURL=governance-reporter.js.map