/**
 * src/approval/manager.ts
 * Approval Manager — Handle tool execution approvals
 */
import { v4 as uuidv4 } from 'uuid';
import { createChildLogger } from '../utils/logger.js';
import { supabase } from '../utils/supabase.js';
import { deliverMessage } from '../delivery/delivery.js';
const log = createChildLogger('approval');
// In-memory pending approvals (for quick access)
const pendingApprovals = new Map();
/**
 * Create an approval request
 */
export async function requestApproval(tenantId, sessionId, toolName, toolArgs, reason, riskLevel = 'medium', expiresInMinutes = 15) {
    const requestId = uuidv4();
    const now = new Date();
    const expiresAt = new Date(now.getTime() + expiresInMinutes * 60 * 1000);
    const { data, error } = await supabase
        .from('approval_requests')
        .insert({
        id: requestId,
        tenant_id: tenantId,
        session_id: sessionId,
        tool_name: toolName,
        tool_args: toolArgs,
        reason,
        risk_level: riskLevel,
        status: 'pending',
        requested_by: 'agent',
        approved_by: null,
        approved_at: null,
        expires_at: expiresAt.toISOString(),
    })
        .select()
        .single();
    if (error) {
        throw new Error(`Failed to create approval request: ${error.message}`);
    }
    pendingApprovals.set(requestId, data);
    // Notify the tenant
    await notifyApprovalRequest(data);
    log.info({ requestId, toolName, riskLevel }, 'Approval request created');
    return data;
}
/**
 * Get pending approval by ID
 */
export async function getApprovalRequest(requestId) {
    const { data, error } = await supabase
        .from('approval_requests')
        .select('*')
        .eq('id', requestId)
        .single();
    if (error) {
        if (error.code === 'PGRST116')
            return null;
        throw new Error(`Database error: ${error.message}`);
    }
    return data;
}
/**
 * Get all pending approvals for a tenant
 */
export async function getPendingApprovals(tenantId) {
    const { data, error } = await supabase
        .from('approval_requests')
        .select('*')
        .eq('tenant_id', tenantId)
        .eq('status', 'pending')
        .gte('expires_at', new Date().toISOString());
    if (error) {
        throw new Error(`Database error: ${error.message}`);
    }
    return data.map(r => ({
        requestId: r.id,
        toolName: r.tool_name,
        args: r.tool_args,
        reason: r.reason,
        riskLevel: r.risk_level,
        expiresAt: r.expires_at,
    }));
}
/**
 * Approve or deny a request
 */
export async function decideApproval(requestId, decision, approvedBy) {
    const request = await getApprovalRequest(requestId);
    if (!request) {
        throw new Error(`Approval request not found: ${requestId}`);
    }
    if (request.status !== 'pending') {
        throw new Error(`Approval request already decided: ${request.status}`);
    }
    const now = new Date().toISOString();
    const newStatus = decision.approved ? 'approved' : 'denied';
    const { data, error } = await supabase
        .from('approval_requests')
        .update({
        status: newStatus,
        approved_by: approvedBy,
        approved_at: newStatus === 'approved' ? now : null,
        updated_at: now,
    })
        .eq('id', requestId)
        .select()
        .single();
    if (error) {
        throw new Error(`Failed to update approval: ${error.message}`);
    }
    pendingApprovals.delete(requestId);
    log.info({ requestId, decision: newStatus, approvedBy }, 'Approval decided');
    // Post-approval side effects — dispatch by tool_name when the merchant
    // actually approved (denials are no-ops). New tools that need approval
    // delivery hooks add a case here.
    if (newStatus === 'approved') {
        try {
            if (request.tool_name === 'zatca_invoice') {
                const { deliverApprovedZatcaInvoice } = await import('../integrations/zatca/invoice.js');
                await deliverApprovedZatcaInvoice(request.tenant_id, request.id);
            }
        }
        catch (err) {
            log.error({ requestId, tool: request.tool_name, error: err instanceof Error ? err.message : String(err) }, 'Post-approval dispatch failed — invoice/tool may be left in inconsistent state');
        }
    }
    return data;
}
/**
 * Notify tenant of pending approval (via WhatsApp)
 */
async function notifyApprovalRequest(request) {
    const tenant = await getTenantById(request.tenant_id);
    if (!tenant) {
        log.warn({ tenantId: request.tenant_id }, 'Tenant not found for approval notification');
        return;
    }
    const riskEmoji = {
        low: '🟢',
        medium: '🟡',
        high: '🟠',
        critical: '🔴',
    }[request.risk_level] || '⚪';
    // Redact known-sensitive arg keys before previewing — prevents secrets
    // leaking via WhatsApp notifications + stored approval rows.
    const SENSITIVE_KEYS = /password|secret|token|api[_-]?key|access[_-]?token|refresh[_-]?token|private[_-]?key|credit[_-]?card|cvv|otp/i;
    const redacted = {};
    for (const [k, v] of Object.entries(request.tool_args || {})) {
        redacted[k] = SENSITIVE_KEYS.test(k) ? '***REDACTED***' : v;
    }
    const argsPreview = JSON.stringify(redacted, null, 2).slice(0, 200);
    const message = `
${riskEmoji} *Approval Required*

Tool: \`${request.tool_name}\`
Risk: ${request.risk_level.toUpperCase()}

Reason: ${request.reason}

Args:
\`\`\`
${argsPreview}
\`\`\`

Reply with:
- ✅ to approve
- ❌ to deny
- ⏰ to extend timeout

This request expires in 15 minutes.
`.trim();
    await deliverMessage({
        tenantId: tenant.id,
        to: tenant.whatsapp_phone,
        message,
        channel: 'whatsapp',
    });
}
/**
 * Helper to check if a tool requires approval
 */
export function requiresApproval(toolName) {
    const dangerousTools = [
        'execute_command',
        'write_file',
        'delete_file',
        'delete_directory',
        'send_email',
        'post_tweet',
        'webhook_call',
        'api_call',
    ];
    return dangerousTools.includes(toolName);
}
/**
 * Helper to determine risk level
 */
export function getRiskLevel(toolName, args) {
    const criticalTools = ['delete_file', 'delete_directory', 'execute_command'];
    const highRiskTools = ['write_file', 'send_email', 'post_tweet'];
    if (criticalTools.includes(toolName))
        return 'critical';
    if (highRiskTools.includes(toolName))
        return 'high';
    if (toolName === 'execute_command') {
        const cmd = args.command || '';
        if (cmd.includes('rm -rf') || cmd.includes('chmod 777'))
            return 'critical';
        if (cmd.includes('rm ') || cmd.includes('chmod '))
            return 'high';
        return 'medium';
    }
    return 'medium';
}
// Helper to get tenant (imported from manager)
async function getTenantById(tenantId) {
    const { data, error } = await supabase
        .from('tenants')
        .select('*')
        .eq('id', tenantId)
        .single();
    if (error || !data)
        return null;
    return data;
}
/**
 * Handle approval decision from user message
 * Call this when a user replies to an approval request
 */
export async function handleApprovalResponse(requestId, userMessage, approvedBy) {
    const request = await getApprovalRequest(requestId);
    if (!request) {
        return { success: false, message: 'Approval request not found' };
    }
    if (request.status !== 'pending') {
        return { success: false, message: `Request already decided: ${request.status}` };
    }
    const decision = {
        approved: userMessage.includes('✅') || userMessage.toLowerCase().includes('approve') || userMessage.toLowerCase().includes('yes'),
        reason: userMessage,
    };
    if (!decision.approved && !userMessage.toLowerCase().includes('deny') && !userMessage.toLowerCase().includes('no')) {
        // Ambiguous response
        return { success: false, message: 'Please reply with ✅ to approve or ❌ to deny' };
    }
    await decideApproval(requestId, decision, approvedBy);
    return {
        success: true,
        message: decision.approved ? 'Approval granted' : 'Approval denied',
    };
}
//# sourceMappingURL=manager.js.map