/**
 * ApprovalGateEnforcer -- Mandatory approval gates with per-class policy.
 *
 * Categorises actions into six explicit approval classes (destructive,
 * irreversible, financial, data-export, network-egress, secret-rotation),
 * applies per-class disposition policy (requires-approval /
 * auto-approve-low-risk / always-deny), and blocks synchronously until a
 * human approves, rejects, or the configured timeout elapses.
 *
 * RELATIONSHIP TO PR #24 (feat/p3-t1-autonomy-spectrum):
 *   PR #24 ships AutonomySpectrumEnforcer (Level 0-4). At Level 4 certain
 *   action classes are routed to an approval queue via `guardAction()` which
 *   returns `{ decision: 'approval-required', approvalRequestId }`. The
 *   Level 4 gate intentionally does NOT own the queue lifecycle -- it only
 *   names the classes that need gating.
 *
 *   THIS MODULE owns the gate lifecycle:
 *     - Explicit class definitions with rationale (action-classes.ts)
 *     - Per-class disposition config (ClassPolicyMap)
 *     - Synchronous blocking via Promise + polling (no busy-spin)
 *     - Audit chain writes on every decision
 *     - Replay protection in the store (PT-APPROVAL-3)
 *     - Empty packBinding[] guard (PT-APPROVAL-1)
 *     - Config validation at construction (PT-APPROVAL-2)
 *     - Authorised-approver list enforcement (PT-APPROVAL-4)
 *
 * HOOK POSITION:
 *   The gate should fire AFTER blast-radius enforcement (PR #42) but BEFORE
 *   action-isolation (PR #29). Rationale: blast-radius has already confirmed
 *   the action is within scope; the gate then checks whether it needs human
 *   sign-off before it executes. Action-isolation runs post-approval to
 *   sandbox the approved execution. As of main HEAD, PRs #42 and #29 are
 *   not yet merged, so this ordering is documented here but has no direct
 *   code coupling to either module.
 *
 * TIMEOUT SEMANTICS:
 *   On timeout, the safe default is DENY. Operators may override per-class
 *   via `ActionClassPolicy.timeoutMs`. There is no "allow on timeout" option.
 *
 * COMPLIANCE BINDINGS:
 *   HIPAA 164.308(a)(1): Security management process
 *   HIPAA 164.308(a)(3): Workforce authorisation and supervision
 *   HIPAA 164.312(a)(1): Access controls
 *   SOC 2 CC6.1: Logical access restrictions
 *   GDPR Art. 22: Automated decision-making
 *   PCI DSS Req 7: Restrict access by business need
 *   NIST 800-53 AC-3: Access enforcement
 *   ISO 27001 A.9.4.1: Information access restriction
 *
 * @connexum/ai-governance P3-T2
 */
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import { UnboundDataError } from '../errors/index.js';
import { ActionClass, buildDefaultClassPolicies, validateClassPolicies, ConfigValidationError, } from './action-classes.js';
import { InMemoryApprovalQueueStore, } from './approval-queue-store.js';
// Re-export for consumers that only import from the enforcer file.
export { ActionClass, ConfigValidationError };
// ---------------------------------------------------------------------------
// ApprovalGateEnforcer
// ---------------------------------------------------------------------------
export class ApprovalGateEnforcer {
    cfg;
    /**
     * PT-APPROVAL-2: constructor validates the class policy map.
     * Throws ConfigValidationError immediately on invalid configuration.
     */
    constructor(config = {}) {
        // Merge caller-supplied partial policies over the defaults.
        const defaults = buildDefaultClassPolicies();
        const merged = config.classPolicies
            ? { ...defaults, ...config.classPolicies }
            : defaults;
        // Validate before storing.
        validateClassPolicies(merged);
        // Additional config validation.
        const defaultTimeoutMs = config.defaultTimeoutMs ?? 600_000;
        if (!Number.isFinite(defaultTimeoutMs) || defaultTimeoutMs <= 0) {
            throw new ConfigValidationError(`Invalid defaultTimeoutMs "${defaultTimeoutMs}". Must be a positive finite number.`);
        }
        const pollIntervalMs = config.pollIntervalMs ?? 200;
        if (!Number.isFinite(pollIntervalMs) || pollIntervalMs <= 0) {
            throw new ConfigValidationError(`Invalid pollIntervalMs "${pollIntervalMs}". Must be a positive finite number.`);
        }
        this.cfg = {
            classPolicies: merged,
            defaultTimeoutMs,
            pollIntervalMs,
            store: config.store ?? new InMemoryApprovalQueueStore(),
            auditDir: config.auditDir ?? null,
        };
        if (this.cfg.auditDir) {
            fs.mkdirSync(this.cfg.auditDir, { recursive: true });
        }
    }
    // ---------------------------------------------------------------------------
    // Primary enforcement API
    // ---------------------------------------------------------------------------
    /**
     * Enforce the approval gate for a given action.
     *
     * Behaviour by disposition:
     *   auto-approve-low-risk  -- Returns immediately with allowed=true.
     *   always-deny            -- Returns immediately with allowed=false.
     *   requires-approval      -- Blocks (polls) until approved/rejected/timeout.
     *
     * PT-APPROVAL-1: empty packBinding throws UnboundDataError before any
     * queue insertion.
     *
     * @param actionContext  Describes the action and its governance context.
     * @returns EnforcementResult promise that resolves when a decision is made.
     */
    async enforce(actionContext) {
        // PT-APPROVAL-1: pack binding must be non-empty.
        if (!actionContext.packBinding || actionContext.packBinding.length === 0) {
            throw new UnboundDataError(`approval-gate:enforce[${actionContext.actionClass}]`, actionContext.tenantId ?? 'unknown');
        }
        const policy = this.cfg.classPolicies[actionContext.actionClass];
        const now = new Date().toISOString();
        // ---- auto-approve-low-risk ----
        if (policy.disposition === 'auto-approve-low-risk') {
            const result = {
                allowed: true,
                outcome: 'auto-approved',
                decidedAt: now,
                reason: policy.rationale ?? 'Auto-approved: class policy is auto-approve-low-risk.',
            };
            this.writeAudit({
                type: 'APPROVAL_GATE_AUTO_APPROVED',
                actionClass: actionContext.actionClass,
                initiator: actionContext.initiator,
                packBinding: actionContext.packBinding,
                tenantId: actionContext.tenantId,
                reason: result.reason,
                timestamp: now,
            });
            return result;
        }
        // ---- always-deny ----
        if (policy.disposition === 'always-deny') {
            const result = {
                allowed: false,
                outcome: 'always-denied',
                decidedAt: now,
                reason: policy.rationale ?? 'Denied: class policy is always-deny.',
            };
            this.writeAudit({
                type: 'APPROVAL_GATE_ALWAYS_DENIED',
                actionClass: actionContext.actionClass,
                initiator: actionContext.initiator,
                packBinding: actionContext.packBinding,
                tenantId: actionContext.tenantId,
                reason: result.reason,
                timestamp: now,
            });
            return result;
        }
        // ---- requires-approval ----
        const timeoutMs = policy.timeoutMs ?? this.cfg.defaultTimeoutMs;
        const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
        const approvalId = this.cfg.store.insert({
            createdAt: now,
            expiresAt,
            actionClass: actionContext.actionClass,
            actionDescription: actionContext.description,
            initiator: actionContext.initiator,
            packBinding: actionContext.packBinding,
        });
        this.writeAudit({
            type: 'APPROVAL_GATE_QUEUED',
            approvalId,
            actionClass: actionContext.actionClass,
            initiator: actionContext.initiator,
            packBinding: actionContext.packBinding,
            tenantId: actionContext.tenantId,
            timestamp: now,
        });
        // Block (poll) until resolved or timed out.
        const decision = await this.waitForDecision(approvalId, timeoutMs);
        const record = this.cfg.store.get(approvalId);
        const decidedAt = record.decidedAt ?? new Date().toISOString();
        if (decision === 'timed-out') {
            this.writeAudit({
                type: 'APPROVAL_GATE_TIMED_OUT',
                approvalId,
                actionClass: actionContext.actionClass,
                initiator: actionContext.initiator,
                packBinding: actionContext.packBinding,
                tenantId: actionContext.tenantId,
                reason: 'Timed out. Default action: deny.',
                timestamp: decidedAt,
            });
            return {
                allowed: false,
                outcome: 'timed-out',
                approvalId,
                decidedAt,
                reason: 'Timed out waiting for human approval. Default action: deny.',
            };
        }
        if (decision === 'approved') {
            this.writeAudit({
                type: 'APPROVAL_GATE_APPROVED',
                approvalId,
                actionClass: actionContext.actionClass,
                initiator: actionContext.initiator,
                packBinding: actionContext.packBinding,
                tenantId: actionContext.tenantId,
                approverId: record.approverId,
                reason: record.reason,
                timestamp: decidedAt,
            });
            return {
                allowed: true,
                outcome: 'approved',
                approvalId,
                approverId: record.approverId,
                reason: record.reason,
                decidedAt,
            };
        }
        // rejected
        this.writeAudit({
            type: 'APPROVAL_GATE_REJECTED',
            approvalId,
            actionClass: actionContext.actionClass,
            initiator: actionContext.initiator,
            packBinding: actionContext.packBinding,
            tenantId: actionContext.tenantId,
            approverId: record.approverId,
            reason: record.reason,
            timestamp: decidedAt,
        });
        return {
            allowed: false,
            outcome: 'rejected',
            approvalId,
            approverId: record.approverId,
            reason: record.reason,
            decidedAt,
        };
    }
    // ---------------------------------------------------------------------------
    // Approval submission API (caller-side hooks for human approver)
    // ---------------------------------------------------------------------------
    /**
     * Submit an approval decision for a pending gate.
     *
     * PT-APPROVAL-3: If the approvalId has already been decided, this call
     * is a silent no-op (the store's replay protection handles it).
     *
     * PT-APPROVAL-4: If the class policy specifies `allowedApprovers` and
     * `approverId` is not in that list, the approval is rejected and an
     * APPROVAL_GATE_UNAUTHORIZED_APPROVER audit entry is written.
     *
     * @param approvalId  The ID returned by `enforce()`.
     * @param approverId  Identity of the human approver.
     * @param reason      Optional human-readable reason.
     */
    submitApproval(approvalId, approverId, reason) {
        const record = this.cfg.store.get(approvalId);
        if (!record)
            return;
        // PT-APPROVAL-4: check authorised approvers.
        if (!this.isAuthorizedApprover(record.actionClass, approverId)) {
            this.writeAudit({
                type: 'APPROVAL_GATE_UNAUTHORIZED_APPROVER',
                approvalId,
                actionClass: record.actionClass,
                initiator: record.initiator,
                packBinding: record.packBinding,
                approverId,
                reason: `Approver "${approverId}" is not in the allowedApprovers list for class "${record.actionClass}".`,
                timestamp: new Date().toISOString(),
            });
            // Reject the attempt rather than silently ignoring -- fail closed.
            this.cfg.store.resolve(approvalId, 'rejected', `system:unauthorized:${approverId}`, `Approver "${approverId}" is not authorised for class "${record.actionClass}".`);
            return;
        }
        this.cfg.store.resolve(approvalId, 'approved', approverId, reason);
    }
    /**
     * Submit a rejection decision for a pending gate.
     *
     * PT-APPROVAL-3: If the approvalId has already been decided, this call
     * is a silent no-op.
     *
     * @param approvalId  The ID returned by `enforce()`.
     * @param approverId  Identity of the rejecting human.
     * @param reason      Optional human-readable reason.
     */
    submitRejection(approvalId, approverId, reason) {
        const record = this.cfg.store.get(approvalId);
        if (!record)
            return;
        this.cfg.store.resolve(approvalId, 'rejected', approverId, reason);
    }
    // ---------------------------------------------------------------------------
    // Query API
    // ---------------------------------------------------------------------------
    /**
     * Return all currently pending approvals.
     * Optionally filter by action class or initiator.
     * Used by the dashboard / UI to display the approval queue.
     */
    getPendingApprovals(filter) {
        return this.cfg.store.getPending(filter);
    }
    /**
     * Return a snapshot of all approvals (pending and decided).
     * Useful for audit queries.
     */
    getAllApprovals() {
        return this.cfg.store.getAll();
    }
    /**
     * Retrieve a single approval by ID.
     */
    getApproval(approvalId) {
        return this.cfg.store.get(approvalId);
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    /**
     * Poll the store until the approval is no longer 'pending' or timeout elapses.
     *
     * Uses setInterval-based polling (no busy-spin per AIAG-013).
     * On timeout, calls `store.timeout()` to mark the record, then resolves.
     */
    waitForDecision(approvalId, timeoutMs) {
        return new Promise((resolve) => {
            const deadline = Date.now() + timeoutMs;
            const interval = setInterval(() => {
                const record = this.cfg.store.get(approvalId);
                if (!record) {
                    clearInterval(interval);
                    resolve('rejected');
                    return;
                }
                if (record.status === 'approved') {
                    clearInterval(interval);
                    resolve('approved');
                    return;
                }
                if (record.status === 'rejected') {
                    clearInterval(interval);
                    resolve('rejected');
                    return;
                }
                if (record.status === 'timed-out') {
                    clearInterval(interval);
                    resolve('timed-out');
                    return;
                }
                // Still pending -- check if we're past deadline.
                if (Date.now() >= deadline) {
                    clearInterval(interval);
                    this.cfg.store.timeout(approvalId);
                    resolve('timed-out');
                }
            }, this.cfg.pollIntervalMs);
        });
    }
    /**
     * PT-APPROVAL-4: Returns true if the approverId is permitted to approve
     * for the given action class. Always returns true when no allowedApprovers
     * constraint is configured for that class.
     */
    isAuthorizedApprover(actionClass, approverId) {
        const policy = this.cfg.classPolicies[actionClass];
        if (!policy.allowedApprovers || policy.allowedApprovers.length === 0) {
            return true;
        }
        return policy.allowedApprovers.includes(approverId);
    }
    /**
     * Append an audit entry to the JSONL file when auditDir is configured.
     * Errors are logged to stderr but never thrown -- the gate must not fail
     * because the audit writer has a transient I/O error.
     */
    writeAudit(entry) {
        if (!this.cfg.auditDir)
            return;
        const filePath = path.join(this.cfg.auditDir, 'approval-gate-audit.jsonl');
        try {
            fs.appendFileSync(filePath, JSON.stringify(entry) + '\n');
        }
        catch (err) {
            process.stderr.write(`[APPROVAL GATE AUDIT] Failed to write audit entry: ${err.message}\n`);
        }
    }
}
// ---------------------------------------------------------------------------
// generateApprovalId -- exported helper for tests / integrations
// ---------------------------------------------------------------------------
/**
 * Generate a unique approval ID in the same format used by the enforcer.
 * Exposed for testing and integration code that needs to pre-generate IDs.
 */
export function generateApprovalId() {
    return `apg_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
}
//# sourceMappingURL=approval-gate-enforcer.js.map