/**
 * Org-level policy inheritance for workspace-scoped compliance configuration.
 *
 * Pack-binding primacy: every policy decision traces to the compliance pack.
 * The pack supplies the policy floor. Tenants may ADD stricter overrides on
 * top via TenantPolicyOverride but NEVER loosen below the pack minimum.
 *
 * Strictest-wins resolution order:
 *   1. Workspace override (if present and STRICTER than pack floor)
 *   2. Pack default (from pack.retentionPolicy / pack.moduleConfig)
 *   3. System default (if neither workspace nor pack specifies)
 *
 * Integration note (P4-RET-02):
 *   PolicyMatrix calls resolveInherited() to determine effective retention
 *   settings. If P4-RET-02 has not merged yet, the stub below returns the
 *   pack default directly.
 *
 * @connexum/ai-governance tenant substrate
 */
import * as crypto from 'node:crypto';
// ---------------------------------------------------------------------------
// System defaults (lowest priority in the resolution chain)
// ---------------------------------------------------------------------------
const SYSTEM_DEFAULTS = {
    retentionDays: 365,
    minConfidenceAutonomous: 0.8,
    tokenBudgetPerCycle: 50_000,
    timeoutMs: 30_000,
    approvalTier: 'reviewer',
};
// ---------------------------------------------------------------------------
// Error classes
// ---------------------------------------------------------------------------
export class PolicyWeakeningRefusedError extends Error {
    field;
    packFloor;
    attempted;
    constructor(field, packFloor, attempted) {
        super(`Policy weakening refused for field '${field}': pack floor is ${String(packFloor)}, ` +
            `attempted override ${String(attempted)} would loosen the requirement`);
        this.name = 'PolicyWeakeningRefusedError';
        this.field = field;
        this.packFloor = packFloor;
        this.attempted = attempted;
    }
}
// ---------------------------------------------------------------------------
// Pack floor extraction
// ---------------------------------------------------------------------------
/**
 * Extract the pack-mandated floor value for a given override field.
 * Returns undefined if the pack does not specify a floor for this field.
 */
function extractPackFloor(pack, field) {
    switch (field) {
        case 'retentionDays':
            // Use the strictest of the three retention windows the pack specifies.
            return Math.max(pack.retentionPolicy.auditLogRetentionDays, pack.retentionPolicy.sessionRetentionDays, pack.retentionPolicy.incidentRetentionDays);
        case 'minConfidenceAutonomous': {
            // Pull from moduleConfig['confidence-gate'] if present.
            const cfg = pack.moduleConfig?.['confidence-gate'];
            return cfg?.minConfidenceAutonomous ?? undefined;
        }
        case 'tokenBudgetPerCycle': {
            const cfg = pack.moduleConfig?.['cycle-token-budget'];
            return cfg?.tokenBudgetPerCycle ?? undefined;
        }
        case 'timeoutMs': {
            const cfg = pack.moduleConfig?.['cycle-timeout'];
            return cfg?.timeoutMs ?? undefined;
        }
        case 'approvalTier': {
            const cfg = pack.moduleConfig?.['approval-queue'];
            return cfg?.approvalTier ?? undefined;
        }
        default:
            return undefined;
    }
}
// ---------------------------------------------------------------------------
// Strictness comparison
// ---------------------------------------------------------------------------
/**
 * Returns true if `proposed` is AT LEAST AS STRICT as `floor` for the given
 * field. "Stricter" semantics are field-specific:
 *
 *   retentionDays          -- proposed >= floor (more days = stricter)
 *   minConfidenceAutonomous -- proposed >= floor (higher confidence = stricter)
 *   tokenBudgetPerCycle    -- proposed <= floor (lower budget = stricter)
 *   timeoutMs              -- proposed <= floor (shorter timeout = stricter)
 *   approvalTier           -- string equality or tier hierarchy not enforced
 *                            (string fields are permitted as-is;
 *                             tier hierarchy is a product decision deferred to v2)
 */
function isStrictEnough(field, floor, proposed) {
    if (floor === undefined || floor === null)
        return true; // no pack floor -> always ok
    switch (field) {
        case 'retentionDays':
        case 'minConfidenceAutonomous': {
            const f = Number(floor);
            const p = Number(proposed);
            if (isNaN(f) || isNaN(p))
                return true;
            return p >= f;
        }
        case 'tokenBudgetPerCycle':
        case 'timeoutMs': {
            const f = Number(floor);
            const p = Number(proposed);
            if (isNaN(f) || isNaN(p))
                return true;
            return p <= f;
        }
        case 'approvalTier':
            // String fields: any value is accepted (approval tier hierarchy TBD).
            return true;
        default:
            return true;
    }
}
// ---------------------------------------------------------------------------
// PolicyInheritanceResolver
// ---------------------------------------------------------------------------
export class PolicyInheritanceResolver {
    policies = new Map(); // policyId -> TenantPolicy
    byWorkspacePack = new Map(); // workspaceId:packId -> policyId
    audit;
    /** Pack loader: injected so the resolver can read pack floors. */
    loadPack;
    constructor(opts) {
        this.loadPack = opts.loadPack;
        this.audit = opts.appendAudit;
    }
    async emit(workspaceId, entry) {
        if (!this.audit)
            return;
        try {
            await this.audit(workspaceId, { ...entry, timestamp: new Date().toISOString() });
        }
        catch {
            // Non-blocking.
        }
    }
    wsPackKey(workspaceId, packId) {
        return `${workspaceId}:${packId}`;
    }
    // -----------------------------------------------------------------------
    // Core: resolve effective value
    // -----------------------------------------------------------------------
    /**
     * Resolve the effective policy value for a given workspace, pack, and field.
     *
     * Resolution order (strictest-wins):
     *   1. If workspace has an active override for this pack+field, use it
     *      (the override is guaranteed to be >= pack floor by setOverride).
     *   2. Pack floor (extracted from retentionPolicy / moduleConfig).
     *   3. System default.
     *
     * TODO(P4-RET-02): When PolicyMatrix lands, it calls this method.
     * Until then, stub: returns pack floor when no workspace override exists.
     */
    resolveInherited(workspaceId, packId, field) {
        const key = this.wsPackKey(workspaceId, packId);
        const policyId = this.byWorkspacePack.get(key);
        const pack = this.loadPack(packId);
        const packFloor = pack ? extractPackFloor(pack, field) : undefined;
        // Check if workspace has an override for this field
        if (policyId) {
            const policy = this.policies.get(policyId);
            if (policy && this.isPolicyActive(policy)) {
                const override = policy.overrides.find(o => o.field === field);
                if (override !== undefined) {
                    return override.value;
                }
            }
        }
        // Fall back to pack floor
        if (packFloor !== undefined && packFloor !== null)
            return packFloor;
        // Fall back to system default
        return SYSTEM_DEFAULTS[field];
    }
    isPolicyActive(policy) {
        const now = new Date();
        const from = new Date(policy.activeFromAt);
        if (now < from)
            return false;
        if (policy.activeUntilAt) {
            const until = new Date(policy.activeUntilAt);
            if (now > until)
                return false;
        }
        return true;
    }
    // -----------------------------------------------------------------------
    // Write: create/replace policy with override validation
    // -----------------------------------------------------------------------
    /**
     * Create or replace the TenantPolicy for a workspace+pack combination.
     *
     * Fail-closed: if ANY override would loosen below the pack floor,
     * the entire operation is rejected with PolicyWeakeningRefusedError.
     * No partial writes occur.
     */
    async setOverride(params) {
        const pack = this.loadPack(params.packId);
        // Validate every override against the pack floor before writing.
        for (const override of params.overrides) {
            const floor = pack ? extractPackFloor(pack, override.field) : undefined;
            if (!isStrictEnough(override.field, floor, override.value)) {
                throw new PolicyWeakeningRefusedError(override.field, floor, override.value);
            }
        }
        const policyId = crypto.randomUUID();
        const now = new Date().toISOString();
        const policy = {
            ...params,
            policyId,
            createdAt: now,
        };
        // Remove old policy for this workspace+pack if one existed.
        const key = this.wsPackKey(params.workspaceId, params.packId);
        const oldPolicyId = this.byWorkspacePack.get(key);
        if (oldPolicyId) {
            this.policies.delete(oldPolicyId);
            await this.emit(params.workspaceId, {
                kind: 'tenant:policy-override-deleted',
                policyId: oldPolicyId,
                workspaceId: params.workspaceId,
                packId: params.packId,
                reason: 'replaced by new setOverride call',
            });
        }
        this.policies.set(policyId, policy);
        this.byWorkspacePack.set(key, policyId);
        await this.emit(params.workspaceId, {
            kind: 'tenant:policy-override-created',
            policyId,
            workspaceId: params.workspaceId,
            packId: params.packId,
            overrideCount: params.overrides.length,
            createdBy: params.createdBy,
        });
        return policy;
    }
    /**
     * Update an existing policy's overrides.
     * Same strictest-wins validation applies -- any weakening attempt throws.
     */
    async updateOverride(policyId, updates) {
        const existing = this.policies.get(policyId);
        if (!existing)
            throw new Error(`Policy not found: ${policyId}`);
        const pack = this.loadPack(existing.packId);
        for (const override of updates.overrides) {
            const floor = pack ? extractPackFloor(pack, override.field) : undefined;
            if (!isStrictEnough(override.field, floor, override.value)) {
                throw new PolicyWeakeningRefusedError(override.field, floor, override.value);
            }
        }
        const updated = { ...existing, overrides: updates.overrides };
        this.policies.set(policyId, updated);
        await this.emit(existing.workspaceId, {
            kind: 'tenant:policy-override-updated',
            policyId,
            workspaceId: existing.workspaceId,
            packId: existing.packId,
            updatedBy: updates.updatedBy,
            overrideCount: updates.overrides.length,
        });
        return updated;
    }
    /**
     * Delete a TenantPolicy by ID.
     * After deletion, resolveInherited falls back to pack floor / system default.
     */
    async deleteOverride(policyId, deletedBy) {
        const existing = this.policies.get(policyId);
        if (!existing)
            return; // idempotent
        const key = this.wsPackKey(existing.workspaceId, existing.packId);
        this.byWorkspacePack.delete(key);
        this.policies.delete(policyId);
        await this.emit(existing.workspaceId, {
            kind: 'tenant:policy-override-deleted',
            policyId,
            workspaceId: existing.workspaceId,
            packId: existing.packId,
            deletedBy,
        });
    }
    // -----------------------------------------------------------------------
    // Read helpers
    // -----------------------------------------------------------------------
    getPolicy(policyId) {
        return this.policies.get(policyId) ?? null;
    }
    listPolicies(workspaceId) {
        const result = [];
        for (const [key, policyId] of this.byWorkspacePack.entries()) {
            if (key.startsWith(`${workspaceId}:`)) {
                const p = this.policies.get(policyId);
                if (p)
                    result.push(p);
            }
        }
        return result;
    }
}
// ---------------------------------------------------------------------------
// Singleton
// ---------------------------------------------------------------------------
let _resolver = null;
export function getPolicyInheritanceResolver(opts) {
    if (!_resolver)
        _resolver = new PolicyInheritanceResolver(opts);
    return _resolver;
}
/** Test helper. */
export function __resetPolicyResolverForTest() {
    _resolver = null;
}
// ---------------------------------------------------------------------------
// Prisma migration note (flagged for later)
// ---------------------------------------------------------------------------
//
// TODO(Prisma migration): Add model to schema.prisma:
//
// model TenantPolicy {
//   policyId     String   @id @default(uuid())
//   workspaceId  String
//   packId       String
//   overrides    Json     // PolicyOverride[]
//   createdBy    String
//   createdAt    DateTime @default(now())
//   activeFromAt DateTime
//   activeUntilAt DateTime?
//   workspace    Workspace @relation(fields: [workspaceId], references: [workspaceId])
//   @@unique([workspaceId, packId])
// }
//# sourceMappingURL=policy-inheritance.js.map