// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
/**
 * Trust Engine - Behavioral Trust Scoring
 *
 * Calculates and maintains trust scores for entities based on behavioral signals.
 * Persists to PostgreSQL for durability.
 *
 * Supports the dual-layer certification/runtime model:
 * - Certification Layer (CAR): Portable attestations that travel with agents
 * - Runtime Layer (Vorion): Deployment-specific trust enforcement
 *
 * @packageDocumentation
 */
import { eq, and, gte, desc, sql } from 'drizzle-orm';
import { createLogger } from '../common/logger.js';
import { getDatabase } from '../common/db.js';
import { trustRecords, trustSignals, trustHistory, } from '../db/schema/trust.js';
import { TrustEngineError, isVorionError } from '../common/errors.js';
import { trustSignalsRecordedTotal, trustScoreDistribution, recordTrustCalculationMetric, } from '../common/metrics.js';
// CAR Integration imports
import { parseCAR, parseACI, } from '@vorionsys/contracts/car';
import { calculateEffectiveFromACI, attestationToTrustSignal, applyACIFloor, calculateEffectiveTier, calculateEffectiveScore, scoreToTier, } from './car-integration.js';
import { ObservabilityClass, getObservabilityCeiling, determineObservabilityClass, } from './observability.js';
import { getContextCeiling, detectDeploymentContext, } from './context.js';
import { FACTOR_CODE_LIST, DEFAULT_FACTOR_WEIGHTS, SIGNAL_PREFIX_TO_FACTORS as BASIS_SIGNAL_PREFIX_MAP, initialFactorScores, } from '@vorionsys/basis';
const logger = createLogger({ component: 'trust-engine' });
/**
 * Trust level thresholds — canonical 8-tier model
 */
export const TRUST_THRESHOLDS = {
    0: { min: 0, max: 199 },
    1: { min: 200, max: 349 },
    2: { min: 350, max: 499 },
    3: { min: 500, max: 649 },
    4: { min: 650, max: 799 },
    5: { min: 800, max: 875 },
    6: { min: 876, max: 950 },
    7: { min: 951, max: 1000 },
};
/**
 * Trust level names — canonical 8-tier model
 */
export const TRUST_LEVEL_NAMES = {
    0: 'Sandbox',
    1: 'Observed',
    2: 'Provisional',
    3: 'Monitored',
    4: 'Standard',
    5: 'Trusted',
    6: 'Certified',
    7: 'Autonomous',
};
// Re-export canonical factor constants from @vorionsys/basis
export const FACTOR_CODES = FACTOR_CODE_LIST;
export const FACTOR_WEIGHTS = DEFAULT_FACTOR_WEIGHTS;
export const SIGNAL_PREFIX_TO_FACTORS = BASIS_SIGNAL_PREFIX_MAP;
export { initialFactorScores };
/**
 * @deprecated Use FACTOR_WEIGHTS for 16-factor scoring. Kept for backwards compatibility.
 * Signal weights for score calculation
 */
export const SIGNAL_WEIGHTS = {
    behavioral: 0.4,
    compliance: 0.25,
    identity: 0.2,
    context: 0.15,
};
/**
 * Stepped dormancy milestones (local duplicate)
 *
 * Canonical source: @vorionsys/atsf-core/decay-profiles DORMANCY_MILESTONES
 * This copy exists because security does not depend on atsf-core.
 *
 * Trust decays incrementally based on days since last activity.
 * 182-day half-life: after 182 days of inactivity, score is 50% of original.
 *
 * Steps 1-5: 6% drop each (100% → 70%)
 * Steps 6-9: 5% drop each (70% → 50%)
 *
 * 9 milestones, simple and predictable.
 */
export const DORMANCY_MILESTONES = [
    { days: 0, multiplier: 1.00 },
    { days: 7, multiplier: 0.94 },
    { days: 14, multiplier: 0.88 },
    { days: 28, multiplier: 0.82 },
    { days: 42, multiplier: 0.76 },
    { days: 56, multiplier: 0.70 },
    { days: 84, multiplier: 0.65 },
    { days: 112, multiplier: 0.60 },
    { days: 140, multiplier: 0.55 },
    { days: 182, multiplier: 0.50 },
];
/** @deprecated Use DORMANCY_MILESTONES */
export const DECAY_MILESTONES = DORMANCY_MILESTONES;
/**
 * Trust Engine service with PostgreSQL persistence
 *
 * Uses stepped decay milestones (182-day half-life) for trust score degradation.
 *
 * SECURITY: All trust operations now require tenantId for multi-tenant isolation.
 * Cross-tenant queries are prevented by validating entity ownership.
 *
 * @see DORMANCY_MILESTONES
 */
export class TrustEngine {
    db = null;
    initialized = false;
    injectedDb = null;
    /**
     * Entity-to-tenant mapping cache (in production, use Redis or dedicated table)
     * This maps entityId -> tenantId for ownership validation
     */
    entityTenantCache = new Map();
    /**
     * Create a new TrustEngine instance.
     *
     * @param deps - Optional dependencies for dependency injection.
     *               If database is provided, it will be used immediately without lazy init.
     *
     * @example
     * // Default usage (lazy initialization)
     * const engine = new TrustEngine();
     * await engine.initialize();
     *
     * @example
     * // With dependency injection (for testing)
     * const engine = new TrustEngine({ database: mockDb });
     */
    constructor(deps = {}) {
        // If database is injected, mark as initialized
        if (deps.database) {
            this.injectedDb = deps.database;
            this.db = deps.database;
            this.initialized = true;
        }
        // Decay is now handled via DORMANCY_MILESTONES (stepped decay)
    }
    /**
     * Validate that an entity belongs to the specified tenant
     *
     * SECURITY: This prevents cross-tenant data access by ensuring
     * the requesting tenant owns the entity being accessed.
     *
     * @throws TrustEngineError if entity does not belong to tenant
     */
    async validateTenantOwnership(entityId, tenantId) {
        // Check cache first
        const cachedTenant = this.entityTenantCache.get(entityId);
        if (cachedTenant) {
            if (cachedTenant !== tenantId) {
                logger.warn({ entityId, requestedTenantId: tenantId, actualTenantId: cachedTenant }, 'SECURITY: Cross-tenant trust query attempt blocked');
                throw new TrustEngineError('Entity does not belong to the specified tenant', 'validateTenantOwnership', entityId, { tenantId, reason: 'CROSS_TENANT_ACCESS_DENIED' });
            }
            return;
        }
        // In production, query the entity registry or a dedicated mapping table
        // For now, we register entities on first access with their tenant
        // This is a security-first approach: unknown entities are associated with the first tenant that accesses them
        logger.debug({ entityId, tenantId }, 'Entity-tenant mapping not cached, registering association');
        this.entityTenantCache.set(entityId, tenantId);
    }
    /**
     * Register entity-tenant association
     * Call this when creating or importing entities
     */
    registerEntityTenant(entityId, tenantId) {
        this.entityTenantCache.set(entityId, tenantId);
        logger.debug({ entityId, tenantId }, 'Entity-tenant association registered');
    }
    /**
     * Initialize the service
     */
    async initialize() {
        if (this.initialized)
            return;
        // Use injected database if available, otherwise get from singleton
        this.db = this.injectedDb ?? getDatabase();
        this.initialized = true;
        logger.info('Trust engine initialized with database persistence');
    }
    /**
     * Ensure service is initialized
     */
    async ensureInitialized() {
        if (!this.initialized || !this.db) {
            await this.initialize();
        }
        return this.db;
    }
    /**
     * Calculate trust score for an entity
     *
     * SECURITY: Requires tenantId for multi-tenant isolation
     *
     * @param entityId - The entity to calculate trust for
     * @param options - Operation options including tenantId
     */
    async calculate(entityId, options) {
        const startTime = performance.now();
        const tenantId = options?.tenantId ?? 'unknown';
        try {
            const db = await this.ensureInitialized();
            // SECURITY: Validate tenant ownership if tenantId provided
            if (options?.tenantId) {
                await this.validateTenantOwnership(entityId, options.tenantId);
            }
            else {
                logger.warn({ entityId }, 'SECURITY WARNING: calculate() called without tenantId - this will be required in future versions');
            }
            // Get recent signals for the entity (last 7 days for weighted calculation)
            const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
            const signals = await db
                .select()
                .from(trustSignals)
                .where(and(eq(trustSignals.entityId, entityId), gte(trustSignals.timestamp, sevenDaysAgo)))
                .orderBy(desc(trustSignals.timestamp))
                .limit(1000);
            // Convert to domain signals
            const domainSignals = signals.map((s) => ({
                id: s.id,
                entityId: s.entityId,
                type: s.type,
                value: s.value,
                weight: s.weight,
                source: s.source ?? '',
                metadata: s.metadata ?? {},
                timestamp: s.timestamp.toISOString(),
            }));
            // Calculate factor scores (16-factor model)
            const factorScores = this.calculateFactorScores(domainSignals);
            // Calculate weighted total using factor weights
            let score = 0;
            for (const code of FACTOR_CODES) {
                score += factorScores[code] * FACTOR_WEIGHTS[code] * 1000;
            }
            score = Math.round(score);
            // Backwards compat: also compute legacy 4-bucket components
            const components = this.calculateComponents(domainSignals);
            // Clamp to valid range
            const clampedScore = Math.max(0, Math.min(1000, score));
            const level = this.scoreToLevel(clampedScore);
            const factors = this.getSignificantFactors(components);
            logger.debug({ entityId, score: clampedScore, level, components }, 'Trust calculated');
            // Record metrics
            const durationSeconds = (performance.now() - startTime) / 1000;
            recordTrustCalculationMetric(tenantId, 'agent', durationSeconds);
            trustScoreDistribution.observe({ tenant_id: tenantId, trust_level: level.toString() }, clampedScore);
            return {
                score: clampedScore,
                level,
                components,
                factorScores,
                factors,
            };
        }
        catch (error) {
            if (isVorionError(error)) {
                throw error;
            }
            logger.error({ error, entityId }, 'Failed to calculate trust score');
            throw new TrustEngineError(`Failed to calculate trust score: ${error instanceof Error ? error.message : 'Unknown error'}`, 'calculate', entityId, { originalError: error instanceof Error ? error.name : 'Unknown' });
        }
    }
    /**
     * Get trust score for an entity
     *
     * SECURITY: Requires tenantId for multi-tenant isolation
     *
     * @param entityId - The entity to get trust score for
     * @param options - Operation options including tenantId
     */
    async getScore(entityId, options) {
        try {
            const db = await this.ensureInitialized();
            // SECURITY: Validate tenant ownership if tenantId provided
            if (options?.tenantId) {
                await this.validateTenantOwnership(entityId, options.tenantId);
            }
            else {
                logger.warn({ entityId }, 'SECURITY WARNING: getScore() called without tenantId - this will be required in future versions');
            }
            const result = await db
                .select()
                .from(trustRecords)
                .where(eq(trustRecords.entityId, entityId))
                .limit(1);
            if (result.length === 0)
                return undefined;
            const record = result[0];
            // Check if recalculation is needed (older than 1 minute)
            const staleness = Date.now() - record.lastCalculatedAt.getTime();
            if (staleness > 60000) {
                // Recalculate
                const calculation = await this.calculate(entityId);
                // Update record
                await db
                    .update(trustRecords)
                    .set({
                    score: calculation.score,
                    level: calculation.level.toString(),
                    behavioralScore: calculation.components.behavioral,
                    complianceScore: calculation.components.compliance,
                    identityScore: calculation.components.identity,
                    contextScore: calculation.components.context,
                    lastCalculatedAt: new Date(),
                    updatedAt: new Date(),
                })
                    .where(eq(trustRecords.entityId, entityId));
                record.score = calculation.score;
                record.level = calculation.level.toString();
                record.behavioralScore = calculation.components.behavioral;
                record.complianceScore = calculation.components.compliance;
                record.identityScore = calculation.components.identity;
                record.contextScore = calculation.components.context;
                record.lastCalculatedAt = new Date();
            }
            // Get recent signals
            const signals = await db
                .select()
                .from(trustSignals)
                .where(eq(trustSignals.entityId, entityId))
                .orderBy(desc(trustSignals.timestamp))
                .limit(100);
            // Get history
            const history = await db
                .select()
                .from(trustHistory)
                .where(eq(trustHistory.entityId, entityId))
                .orderBy(desc(trustHistory.timestamp))
                .limit(100);
            // Apply stepped decay based on inactivity
            const lastActivityAt = record.lastActivityAt ?? record.lastCalculatedAt;
            const daysSinceActivity = this.calculateInactiveDays(lastActivityAt);
            const decayMultiplier = this.calculateDecayMultiplier(daysSinceActivity);
            const baseScore = record.score;
            const decayedScore = this.applyDecay(baseScore, daysSinceActivity);
            const decayApplied = daysSinceActivity > 0;
            // Recalculate level based on decayed score
            const decayedLevel = this.scoreToLevel(decayedScore);
            logger.debug({
                entityId,
                baseScore,
                decayedScore,
                daysSinceActivity,
                decayMultiplier,
            }, 'Decay applied to trust score');
            const returnComponents = {
                behavioral: record.behavioralScore,
                compliance: record.complianceScore,
                identity: record.identityScore,
                context: record.contextScore,
            };
            // Build factor scores from domain signals (or defaults if none)
            const domainSignalsList = signals.map((s) => ({
                id: s.id,
                entityId: s.entityId,
                type: s.type,
                value: s.value,
                weight: s.weight,
                source: s.source ?? '',
                metadata: s.metadata ?? {},
                timestamp: s.timestamp.toISOString(),
            }));
            const factorScores = domainSignalsList.length > 0
                ? this.calculateFactorScores(domainSignalsList)
                : TrustEngine.initialFactorScores();
            return {
                entityId: record.entityId,
                score: decayedScore,
                level: decayedLevel,
                components: returnComponents,
                factorScores,
                signals: signals.map((s) => ({
                    id: s.id,
                    entityId: s.entityId,
                    type: s.type,
                    value: s.value,
                    weight: s.weight,
                    source: s.source ?? '',
                    metadata: s.metadata ?? {},
                    timestamp: s.timestamp.toISOString(),
                })),
                lastCalculatedAt: record.lastCalculatedAt.toISOString(),
                lastActivityAt: lastActivityAt.toISOString(),
                history: history.map((h) => ({
                    score: h.score,
                    level: parseInt(h.level),
                    reason: h.reason,
                    timestamp: h.timestamp.toISOString(),
                })),
                // Decay information
                decayApplied,
                decayMultiplier,
                baseScore,
                nextMilestone: this.getNextMilestone(daysSinceActivity),
            };
        }
        catch (error) {
            if (isVorionError(error)) {
                throw error;
            }
            logger.error({ error, entityId }, 'Failed to get trust score');
            throw new TrustEngineError(`Failed to get trust score: ${error instanceof Error ? error.message : 'Unknown error'}`, 'getScore', entityId);
        }
    }
    /**
     * Record a trust signal
     *
     * SECURITY: Requires tenantId for multi-tenant isolation
     *
     * @param signal - The trust signal to record
     * @param options - Operation options including tenantId
     */
    async recordSignal(signal, options) {
        try {
            const db = await this.ensureInitialized();
            // SECURITY: Validate tenant ownership if tenantId provided
            if (options?.tenantId) {
                await this.validateTenantOwnership(signal.entityId, options.tenantId);
            }
            else {
                logger.warn({ entityId: signal.entityId }, 'SECURITY WARNING: recordSignal() called without tenantId - this will be required in future versions');
            }
            // Insert the signal
            const newSignal = {
                entityId: signal.entityId,
                type: signal.type,
                value: signal.value,
                weight: signal.weight ?? 1.0,
                source: signal.source ?? null,
                metadata: signal.metadata ?? null,
                timestamp: signal.timestamp ? new Date(signal.timestamp) : new Date(),
            };
            const [insertedSignal] = await db
                .insert(trustSignals)
                .values(newSignal)
                .returning();
            // Get or create trust record
            let record = await db
                .select()
                .from(trustRecords)
                .where(eq(trustRecords.entityId, signal.entityId))
                .limit(1);
            if (record.length === 0) {
                // Create initial record with lastActivityAt for decay tracking
                const nowDate = new Date();
                const initialRecord = {
                    entityId: signal.entityId,
                    score: 200,
                    level: '1',
                    behavioralScore: 0.5,
                    complianceScore: 0.5,
                    identityScore: 0.5,
                    contextScore: 0.5,
                    signalCount: 1,
                    lastCalculatedAt: nowDate,
                    lastActivityAt: nowDate,
                };
                await db.insert(trustRecords).values(initialRecord);
                const newRecord = {
                    ...initialRecord,
                    id: crypto.randomUUID(),
                    score: initialRecord.score ?? 200,
                    level: initialRecord.level ?? '0',
                    behavioralScore: initialRecord.behavioralScore ?? 50,
                    complianceScore: initialRecord.complianceScore ?? 50,
                    identityScore: initialRecord.identityScore ?? 50,
                    contextScore: initialRecord.contextScore ?? 50,
                    signalCount: initialRecord.signalCount ?? 0,
                    lastCalculatedAt: initialRecord.lastCalculatedAt ?? nowDate,
                    createdAt: nowDate,
                    updatedAt: nowDate,
                    lastActivityAt: nowDate,
                    metadata: null,
                };
                record = [newRecord];
            }
            const currentRecord = record[0];
            const previousScore = currentRecord.score;
            const previousLevel = parseInt(currentRecord.level);
            // Recalculate
            const calculation = await this.calculate(signal.entityId);
            // Update record - reset decay clock with lastActivityAt
            const now = new Date();
            await db
                .update(trustRecords)
                .set({
                score: calculation.score,
                level: calculation.level.toString(),
                behavioralScore: calculation.components.behavioral,
                complianceScore: calculation.components.compliance,
                identityScore: calculation.components.identity,
                contextScore: calculation.components.context,
                signalCount: sql `${trustRecords.signalCount} + 1`,
                lastCalculatedAt: now,
                lastActivityAt: now, // Reset decay clock on trust-positive activity
                updatedAt: now,
            })
                .where(eq(trustRecords.entityId, signal.entityId));
            // Record history if significant change
            if (Math.abs(calculation.score - previousScore) >= 10) {
                const historyEntry = {
                    entityId: signal.entityId,
                    score: calculation.score,
                    previousScore,
                    level: calculation.level.toString(),
                    previousLevel: previousLevel.toString(),
                    reason: `Signal: ${signal.type}`,
                    signalId: insertedSignal?.id,
                    timestamp: new Date(),
                };
                await db.insert(trustHistory).values(historyEntry);
            }
            // Record metrics for signal recording
            trustSignalsRecordedTotal.inc({
                signal_type: signal.type,
                tenant_id: options?.tenantId ?? 'unknown',
            });
            logger.debug({
                entityId: signal.entityId,
                signalType: signal.type,
                newScore: calculation.score,
            }, 'Signal recorded');
        }
        catch (error) {
            if (isVorionError(error)) {
                throw error;
            }
            logger.error({ error, entityId: signal.entityId, signalType: signal.type }, 'Failed to record trust signal');
            throw new TrustEngineError(`Failed to record trust signal: ${error instanceof Error ? error.message : 'Unknown error'}`, 'recordSignal', signal.entityId, { signalType: signal.type });
        }
    }
    /**
     * Get trust history for an entity
     *
     * SECURITY: Requires tenantId for multi-tenant isolation
     *
     * @param entityId - The entity to get history for
     * @param options - Operation options including tenantId
     * @param limit - Maximum number of history entries to return (default: 100)
     */
    async getHistory(entityId, options, limit = 100) {
        try {
            const db = await this.ensureInitialized();
            // SECURITY: Validate tenant ownership (REQUIRED)
            await this.validateTenantOwnership(entityId, options.tenantId);
            const history = await db
                .select()
                .from(trustHistory)
                .where(eq(trustHistory.entityId, entityId))
                .orderBy(desc(trustHistory.timestamp))
                .limit(limit);
            return history.map((h) => ({
                score: h.score,
                level: parseInt(h.level),
                reason: h.reason,
                timestamp: h.timestamp.toISOString(),
            }));
        }
        catch (error) {
            if (isVorionError(error)) {
                throw error;
            }
            logger.error({ error, entityId }, 'Failed to get trust history');
            throw new TrustEngineError(`Failed to get trust history: ${error instanceof Error ? error.message : 'Unknown error'}`, 'getHistory', entityId);
        }
    }
    /**
     * Initialize trust for a new entity
     *
     * SECURITY: Requires tenantId for multi-tenant isolation
     *
     * @param entityId - The entity to initialize
     * @param initialLevel - Initial trust level (default: 1)
     * @param options - Operation options including tenantId
     */
    async initializeEntity(entityId, initialLevel = 1, options) {
        try {
            const db = await this.ensureInitialized();
            // SECURITY: Register entity-tenant association if tenantId provided
            if (options?.tenantId) {
                this.registerEntityTenant(entityId, options.tenantId);
            }
            else {
                logger.warn({ entityId }, 'SECURITY WARNING: initializeEntity() called without tenantId - this will be required in future versions');
            }
            const score = TRUST_THRESHOLDS[initialLevel].min;
            const now = new Date();
            const newRecord = {
                entityId,
                score,
                level: initialLevel.toString(),
                behavioralScore: 0.5,
                complianceScore: 0.5,
                identityScore: 0.5,
                contextScore: 0.5,
                signalCount: 0,
                lastCalculatedAt: now,
                lastActivityAt: now,
            };
            await db.insert(trustRecords).values(newRecord);
            // Record initial history
            const historyEntry = {
                entityId,
                score,
                level: initialLevel.toString(),
                reason: 'Initial registration',
                timestamp: now,
            };
            await db.insert(trustHistory).values(historyEntry);
            logger.info({ entityId, initialLevel }, 'Entity trust initialized');
            return {
                entityId,
                score,
                level: initialLevel,
                components: {
                    behavioral: 0.5,
                    compliance: 0.5,
                    identity: 0.5,
                    context: 0.5,
                },
                factorScores: TrustEngine.initialFactorScores(),
                signals: [],
                lastCalculatedAt: now.toISOString(),
                lastActivityAt: now.toISOString(),
                history: [
                    {
                        score,
                        level: initialLevel,
                        reason: 'Initial registration',
                        timestamp: now.toISOString(),
                    },
                ],
                // New entity has no decay
                decayApplied: false,
                decayMultiplier: 1.0,
                baseScore: score,
                nextMilestone: DORMANCY_MILESTONES[1] ?? null,
            };
        }
        catch (error) {
            if (isVorionError(error)) {
                throw error;
            }
            logger.error({ error, entityId, initialLevel }, 'Failed to initialize entity trust');
            throw new TrustEngineError(`Failed to initialize entity trust: ${error instanceof Error ? error.message : 'Unknown error'}`, 'initializeEntity', entityId, { initialLevel });
        }
    }
    /**
     * Convert score to trust level
     */
    scoreToLevel(score) {
        for (const [level, { min, max }] of Object.entries(TRUST_THRESHOLDS)) {
            if (score >= min && score <= max) {
                return parseInt(level);
            }
        }
        return 0;
    }
    /**
     * @deprecated Use calculateFactorScores for 16-factor model. Kept for backwards compatibility.
     * Calculate component scores from signals
     */
    calculateComponents(signals) {
        // Group signals by type
        const behavioral = signals.filter((s) => s.type.startsWith('behavioral.'));
        const compliance = signals.filter((s) => s.type.startsWith('compliance.'));
        const identity = signals.filter((s) => s.type.startsWith('identity.'));
        const context = signals.filter((s) => s.type.startsWith('context.'));
        return {
            behavioral: this.averageSignalValue(behavioral, 0.5),
            compliance: this.averageSignalValue(compliance, 0.5),
            identity: this.averageSignalValue(identity, 0.5),
            context: this.averageSignalValue(context, 0.5),
        };
    }
    /**
     * Calculate per-factor scores from signals.
     * Signals can use either:
     *   - Factor code prefix (e.g. 'CT-COMP.success')
     *   - Legacy bucket prefix (e.g. 'behavioral.success') — mapped to factors via SIGNAL_PREFIX_TO_FACTORS
     */
    calculateFactorScores(signals) {
        const factorSignals = {};
        // Initialize all factors
        for (const code of FACTOR_CODES) {
            factorSignals[code] = [];
        }
        for (const signal of signals) {
            const prefix = signal.type.split('.')[0];
            // Check if it's a direct factor code
            if (FACTOR_CODES.includes(prefix)) {
                factorSignals[prefix].push(signal);
                continue;
            }
            // Check if it's a legacy bucket prefix
            const mappedFactors = SIGNAL_PREFIX_TO_FACTORS[prefix];
            if (mappedFactors) {
                // Distribute signal across mapped factors
                for (const factorCode of mappedFactors) {
                    factorSignals[factorCode].push(signal);
                }
            }
        }
        // Calculate average score for each factor
        const scores = {};
        for (const code of FACTOR_CODES) {
            scores[code] = this.averageSignalValue(factorSignals[code], 0.5);
        }
        return scores;
    }
    /**
     * Build the initial factor scores record with all 16 factors at 0.5 (neutral)
     */
    static initialFactorScores() {
        const scores = {};
        for (const code of FACTOR_CODES) {
            scores[code] = 0.5;
        }
        return scores;
    }
    /**
     * Calculate average signal value with default
     */
    averageSignalValue(signals, defaultValue) {
        if (signals.length === 0)
            return defaultValue;
        // Weight recent signals more heavily
        const now = Date.now();
        let weightedSum = 0;
        let totalWeight = 0;
        for (const signal of signals) {
            const age = now - new Date(signal.timestamp).getTime();
            const timeWeight = Math.exp(-age / (7 * 24 * 60 * 60 * 1000)); // 7-day signal recency weight (distinct from 182-day trust decay)
            const signalWeight = signal.weight ?? 1.0;
            const combinedWeight = timeWeight * signalWeight;
            weightedSum += signal.value * combinedWeight;
            totalWeight += combinedWeight;
        }
        return totalWeight > 0 ? weightedSum / totalWeight : defaultValue;
    }
    /**
     * Get significant factors affecting the score
     */
    getSignificantFactors(components) {
        const factors = [];
        if (components.behavioral < 0.3) {
            factors.push('Low behavioral trust');
        }
        if (components.compliance < 0.3) {
            factors.push('Low compliance score');
        }
        if (components.identity < 0.3) {
            factors.push('Weak identity verification');
        }
        if (components.context < 0.3) {
            factors.push('Unusual context signals');
        }
        return factors;
    }
    /**
     * Calculate decay multiplier based on days since last activity
     *
     * Uses stepped milestones with interpolation for smooth decay.
     * 182-day half-life: after 182 days of inactivity, score is 50% of original.
     */
    calculateDecayMultiplier(daysSinceLastActivity) {
        // Find the applicable milestone and next milestone
        let applicableMilestone = DORMANCY_MILESTONES[0];
        let nextMilestone = null;
        for (let i = 0; i < DORMANCY_MILESTONES.length; i++) {
            if (daysSinceLastActivity >= DORMANCY_MILESTONES[i].days) {
                applicableMilestone = DORMANCY_MILESTONES[i];
                nextMilestone = DORMANCY_MILESTONES[i + 1] ?? null;
            }
        }
        // If beyond final milestone, use final multiplier
        if (!nextMilestone) {
            return applicableMilestone.multiplier;
        }
        // Interpolate between milestones for smooth decay
        const daysIntoMilestone = daysSinceLastActivity - applicableMilestone.days;
        const milestoneDuration = nextMilestone.days - applicableMilestone.days;
        const progress = daysIntoMilestone / milestoneDuration;
        const decayRange = applicableMilestone.multiplier - nextMilestone.multiplier;
        return applicableMilestone.multiplier - decayRange * progress;
    }
    /**
     * Apply decay to a base score
     */
    applyDecay(baseScore, daysSinceLastActivity) {
        const multiplier = this.calculateDecayMultiplier(daysSinceLastActivity);
        return Math.round(baseScore * multiplier);
    }
    /**
     * Calculate days since last activity from a date
     */
    calculateInactiveDays(lastActivityAt) {
        const now = Date.now();
        const lastActivity = lastActivityAt.getTime();
        const msPerDay = 24 * 60 * 60 * 1000;
        return Math.floor((now - lastActivity) / msPerDay);
    }
    /**
     * Get the next decay milestone for an entity
     */
    getNextMilestone(daysSinceLastActivity) {
        for (const milestone of DORMANCY_MILESTONES) {
            if (milestone.days > daysSinceLastActivity) {
                return milestone;
            }
        }
        return null; // Already at or past final milestone
    }
    // ==========================================================================
    // CAR ID Integration Methods
    // ==========================================================================
    /**
     * Get trust context with CAR ID integration
     *
     * Combines CAR ID identity with attestation-based certification and Vorion
     * runtime layer to produce a complete trust context. The effective tier/score
     * is the minimum of all contributing factors.
     *
     * IMPORTANT: Trust tier comes from attestations, NOT the CAR ID itself.
     * The CAR ID is just an identifier; trust is computed at runtime.
     *
     * @param entityId - The entity to get trust context for
     * @param carId - The CAR ID string for the entity
     * @param attestation - Optional attestation for this entity
     * @returns Complete CAR ID trust context with effective permissions
     */
    async getACITrustContext(entityId, carId, attestation) {
        const parsedCarId = parseACI(carId);
        const trustRecord = await this.getScore(entityId);
        const runtimeScore = trustRecord?.score ?? 200;
        const runtimeTier = scoreToTier(runtimeScore);
        // Get observability and context from entity metadata or config
        const observability = await this.getObservabilityClass(entityId);
        const context = await this.getDeploymentContext(entityId);
        const observabilityCeiling = getObservabilityCeiling(observability);
        const contextPolicyCeiling = getContextCeiling(context);
        // Certification tier comes from attestation, NOT the CAR ID
        const certificationTier = attestation?.trustTier ?? 0;
        const hasValidAttestation = attestation !== null && attestation !== undefined &&
            attestation.expiresAt > new Date();
        const effectiveTier = calculateEffectiveTier(certificationTier, parsedCarId.level, runtimeTier, observabilityCeiling, contextPolicyCeiling);
        const effectiveScore = calculateEffectiveScore(certificationTier, runtimeScore, observabilityCeiling, contextPolicyCeiling);
        logger.debug({
            entityId,
            identity: `${parsedCarId.registry}.${parsedCarId.organization}.${parsedCarId.agentClass}`,
            certificationTier,
            hasValidAttestation,
            runtimeTier,
            observabilityCeiling,
            contextPolicyCeiling,
            effectiveTier,
            effectiveScore,
        }, 'Built CAR trust context');
        return {
            car: parsedCarId,
            identity: `${parsedCarId.registry}.${parsedCarId.organization}.${parsedCarId.agentClass}`,
            competenceLevel: parsedCarId.level,
            operationalDomains: [...parsedCarId.domains],
            certificationTier,
            hasValidAttestation,
            attestationExpiresAt: attestation?.expiresAt,
            runtimeTier,
            runtimeScore,
            observabilityCeiling,
            contextPolicyCeiling: contextPolicyCeiling,
            effectiveTier,
            effectiveScore,
        };
    }
    /**
     * Apply CAR ID attestation as trust signal
     *
     * Converts a CAR ID attestation into a trust signal and applies it to
     * the entity's trust record. Also enforces the certification floor -
     * the entity's score cannot fall below their certified tier minimum.
     *
     * @param entityId - The entity to apply attestation to
     * @param attestation - The CAR ID attestation record
     */
    async applyAttestation(entityId, attestation) {
        const signal = attestationToTrustSignal(attestation);
        // Record the attestation as a trust signal
        await this.recordSignal({
            id: signal.id,
            entityId: signal.entityId,
            type: signal.type,
            value: signal.value,
            weight: signal.weight,
            source: signal.source,
            timestamp: signal.timestamp,
            metadata: signal.metadata,
        });
        // Apply floor from certification
        const trustRecord = await this.getScore(entityId);
        if (trustRecord) {
            const flooredScore = applyACIFloor(trustRecord.score, attestation.trustTier);
            if (flooredScore > trustRecord.score) {
                await this.setScore(entityId, flooredScore, 'CAR ID attestation floor');
            }
        }
        logger.info({
            entityId,
            attestationId: attestation.id,
            trustTier: attestation.trustTier,
            issuer: attestation.issuer,
        }, 'Applied CAR ID attestation');
    }
    /**
     * Check if action is allowed under effective permission
     *
     * Evaluates whether an entity has sufficient effective trust to perform
     * an action requiring a specific tier and domains.
     *
     * @param entityId - The entity requesting the action
     * @param carId - The entity's CAR ID string
     * @param requiredTier - Minimum tier required for the action
     * @param requiredDomains - Domains required for the action
     * @returns Permission check result with reason if denied
     */
    async checkEffectivePermission(entityId, carId, requiredTier, requiredDomains) {
        const ctx = await this.getACITrustContext(entityId, carId);
        const effective = calculateEffectiveFromACI(ctx);
        const tierAllowed = effective.tier >= requiredTier;
        const domainsAllowed = requiredDomains.every((d) => effective.domains.includes(d));
        const allowed = tierAllowed && domainsAllowed;
        let reason;
        if (!tierAllowed) {
            reason = `Requires T${requiredTier}, effective is T${effective.tier}`;
        }
        else if (!domainsAllowed) {
            const missingDomains = requiredDomains.filter((d) => !effective.domains.includes(d));
            reason = `Missing required domains: ${missingDomains.join(', ')}`;
        }
        logger.debug({
            entityId,
            requiredTier,
            requiredDomains,
            effectiveTier: effective.tier,
            certifiedDomains: effective.domains,
            allowed,
            reason,
        }, 'Checked effective permission');
        return {
            allowed,
            effectiveTier: effective.tier,
            effectiveScore: effective.score,
            reason,
            ceilingReason: effective.ceilingReason,
        };
    }
    /**
     * Set trust score directly with reason
     *
     * Used internally for applying floors and ceilings from CAR ID.
     *
     * @param entityId - The entity to update
     * @param score - The new trust score
     * @param reason - Reason for the change
     */
    async setScore(entityId, score, reason) {
        const db = await this.ensureInitialized();
        const level = this.scoreToLevel(score);
        const now = new Date();
        // Get current record for history
        const current = await db
            .select()
            .from(trustRecords)
            .where(eq(trustRecords.entityId, entityId))
            .limit(1);
        if (current.length === 0) {
            // Entity doesn't exist, create it
            await this.initializeEntity(entityId, level);
            return;
        }
        const previousScore = current[0].score;
        const previousLevel = parseInt(current[0].level);
        // Update record
        await db
            .update(trustRecords)
            .set({
            score,
            level: level.toString(),
            lastCalculatedAt: now,
            updatedAt: now,
        })
            .where(eq(trustRecords.entityId, entityId));
        // Record history
        const historyEntry = {
            entityId,
            score,
            previousScore,
            level: level.toString(),
            previousLevel: previousLevel.toString(),
            reason,
            timestamp: now,
        };
        await db.insert(trustHistory).values(historyEntry);
        logger.info({ entityId, previousScore, newScore: score, reason }, 'Trust score updated');
    }
    /**
     * Get observability class for an entity
     *
     * Retrieves or determines the observability class from entity metadata.
     *
     * @param entityId - The entity to check
     * @returns The entity's observability class
     */
    async getObservabilityClass(entityId) {
        const db = await this.ensureInitialized();
        // Try to get from entity metadata stored in trust_records
        const record = await db
            .select()
            .from(trustRecords)
            .where(eq(trustRecords.entityId, entityId))
            .limit(1);
        if (record.length > 0) {
            const rawMetadata = record[0].metadata;
            if (rawMetadata) {
                // Convert JSONB stored metadata to ObservabilityMetadata type
                // The database stores dates as ISO strings, so convert if present
                const metadata = {
                    class: rawMetadata.observabilityClass,
                    attestationProvider: rawMetadata.attestationProvider,
                    verificationProof: rawMetadata.verificationProof,
                    sourceCodeUrl: rawMetadata.sourceCodeUrl,
                    lastAuditDate: rawMetadata.lastAuditDate
                        ? new Date(rawMetadata.lastAuditDate)
                        : undefined,
                };
                // Use determineObservabilityClass to infer from metadata
                return determineObservabilityClass(metadata);
            }
        }
        // Default to most restrictive if unknown
        return ObservabilityClass.BLACK_BOX;
    }
    /**
     * Set observability metadata for an entity
     *
     * Updates the trust record with observability information that determines
     * the entity's trust ceiling.
     *
     * @param entityId - The entity to update
     * @param metadata - Observability metadata (class, attestation info, etc.)
     * @returns True if update was successful
     */
    async setObservabilityMetadata(entityId, metadata) {
        const db = await this.ensureInitialized();
        // Convert ObservabilityMetadata to JSONB-compatible format
        // Dates must be stored as ISO strings in JSONB
        const jsonbMetadata = {
            observabilityClass: metadata.class,
            attestationProvider: metadata.attestationProvider,
            verificationProof: metadata.verificationProof,
            sourceCodeUrl: metadata.sourceCodeUrl,
            lastAuditDate: metadata.lastAuditDate?.toISOString(),
        };
        try {
            // Try to update existing record
            const result = await db
                .update(trustRecords)
                .set({
                metadata: jsonbMetadata,
                updatedAt: new Date(),
            })
                .where(eq(trustRecords.entityId, entityId));
            if (result.rowCount === 0) {
                // No existing record, create one with default values
                await db.insert(trustRecords).values({
                    entityId,
                    score: 200, // Default score for new entities
                    level: '1', // Supervised level
                    metadata: jsonbMetadata,
                });
            }
            logger.info({
                entityId,
                observabilityClass: metadata.class,
                hasAttestation: !!metadata.attestationProvider,
                hasVerification: !!metadata.verificationProof,
            }, 'Updated observability metadata for entity');
            return true;
        }
        catch (error) {
            logger.error({
                entityId,
                error: error instanceof Error ? error.message : String(error),
            }, 'Failed to update observability metadata');
            return false;
        }
    }
    /**
     * Get full observability metadata for an entity
     *
     * @param entityId - The entity to query
     * @returns The observability metadata or undefined if not set
     */
    async getObservabilityMetadata(entityId) {
        const db = await this.ensureInitialized();
        const record = await db
            .select({ metadata: trustRecords.metadata })
            .from(trustRecords)
            .where(eq(trustRecords.entityId, entityId))
            .limit(1);
        if (record.length > 0 && record[0].metadata) {
            const rawMetadata = record[0].metadata;
            // Convert JSONB stored metadata to ObservabilityMetadata type
            // The database stores dates as ISO strings, so convert if present
            return {
                class: rawMetadata.observabilityClass ?? ObservabilityClass.BLACK_BOX,
                attestationProvider: rawMetadata.attestationProvider,
                verificationProof: rawMetadata.verificationProof,
                sourceCodeUrl: rawMetadata.sourceCodeUrl,
                lastAuditDate: rawMetadata.lastAuditDate
                    ? new Date(rawMetadata.lastAuditDate)
                    : undefined,
            };
        }
        return undefined;
    }
    /**
     * Get deployment context for an entity
     *
     * Retrieves or determines the deployment context for trust calculations.
     *
     * @param entityId - The entity to check (may have context override)
     * @returns The applicable deployment context
     */
    async getDeploymentContext(_entityId) {
        // First check for entity-specific context override
        // (could be stored in entity metadata or configuration)
        // For now, detect from environment
        return detectDeploymentContext();
    }
}
/**
 * Create a new Trust Engine instance with dependency injection.
 *
 * This is the preferred way to create trust engines in production code
 * as it makes dependencies explicit and testable.
 *
 * @param deps - Optional dependencies. If database provided, skips lazy init.
 * @returns Configured TrustEngine instance
 *
 * @example
 * // Default usage (lazy initialization)
 * const engine = createTrustEngine();
 * await engine.initialize();
 *
 * @example
 * // With custom dependencies (pre-initialized)
 * const engine = createTrustEngine({ database: customDb });
 */
export function createTrustEngine(deps = {}) {
    return new TrustEngine(deps);
}
// ============================================================================
// Standalone decay functions (exported for unit testing)
// ============================================================================
/**
 * Calculate decay multiplier based on days since last activity
 *
 * Uses stepped milestones with linear interpolation for smooth decay.
 *
 * @param daysSinceLastActivity - Number of days since last trust-positive activity
 * @returns Decay multiplier between 0.5 and 1.0
 */
export function calculateDecayMultiplier(daysSinceLastActivity) {
    // Find the applicable milestone and next milestone
    let applicableMilestone = DORMANCY_MILESTONES[0];
    let nextMilestone = null;
    for (let i = 0; i < DORMANCY_MILESTONES.length; i++) {
        if (daysSinceLastActivity >= DORMANCY_MILESTONES[i].days) {
            applicableMilestone = DORMANCY_MILESTONES[i];
            nextMilestone = DORMANCY_MILESTONES[i + 1] ?? null;
        }
    }
    // If beyond final milestone, use final multiplier
    if (!nextMilestone) {
        return applicableMilestone.multiplier;
    }
    // Interpolate between milestones for smooth decay
    const daysIntoMilestone = daysSinceLastActivity - applicableMilestone.days;
    const milestoneDuration = nextMilestone.days - applicableMilestone.days;
    const progress = daysIntoMilestone / milestoneDuration;
    const decayRange = applicableMilestone.multiplier - nextMilestone.multiplier;
    return applicableMilestone.multiplier - decayRange * progress;
}
/**
 * Apply decay multiplier to a base score
 *
 * @param baseScore - The undecayed trust score
 * @param daysSinceLastActivity - Number of days since last activity
 * @returns Decayed score (rounded to nearest integer)
 */
export function applyDecay(baseScore, daysSinceLastActivity) {
    const multiplier = calculateDecayMultiplier(daysSinceLastActivity);
    return Math.round(baseScore * multiplier);
}
/**
 * Get the next decay milestone for a given number of inactive days
 *
 * @param daysSinceLastActivity - Current days of inactivity
 * @returns Next milestone or null if past final milestone
 */
export function getNextDecayMilestone(daysSinceLastActivity) {
    for (const milestone of DORMANCY_MILESTONES) {
        if (milestone.days > daysSinceLastActivity) {
            return milestone;
        }
    }
    return null;
}
export { parseCAR, parseACI };
export { AttestationSchema, calculateEffectiveFromCAR, calculateEffectiveFromACI, attestationToTrustSignal, applyCARFloor, applyACIFloor, enforceCARCeiling, enforceACICeiling, calculateEffectiveTier, calculateEffectiveScore, scoreToTier, certificationTierToMinScore, certificationTierToMaxScore, certificationTierToScore, tierToMinScore, competenceLevelToCeiling, determineCeilingReason, } from './car-integration.js';
// Re-export from observability.ts
export { ObservabilityClass, OBSERVABILITY_CEILINGS, OBSERVABILITY_CLASS_NAMES, ObservabilityClassSchema, ObservabilityMetadataSchema, getObservabilityCeiling, getObservabilityMaxScore, applyObservabilityCeiling, isTierAllowedForObservability, getRequiredObservabilityForTier, determineObservabilityClass, describeObservabilityConstraints, } from './observability.js';
// Re-export from context.ts
export { DeploymentContext, CONTEXT_CEILINGS, CONTEXT_NAMES, DeploymentContextSchema, ContextConfigSchema, getContextCeiling, getContextMaxScore, applyContextCeiling, requiresHumanApproval, requiresAttestation, evaluateContextPolicy, describeContextConstraints, detectDeploymentContext, } from './context.js';
//# sourceMappingURL=index.js.map