/**
 * معالج رسائل واتساب — WhatsApp Message Handler
 * يستقبل الرسائل الواردة ويعالجها ويرد عليها
 */
import { randomUUID } from 'node:crypto';
import { writeFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createChildLogger } from '../../utils/logger.js';
import { supabase } from '../../utils/supabase.js';
import { whatsappClient } from './client.js';
import { processMessage as brainProcessMessage } from '../../agent/brain.js';
import { processIncomingMedia, prepareOutgoingMedia } from '../../media/index.js';
import { learnFromConversation } from '../../knowledge/unified.js';
import { trackMessage } from '../../analytics/tracker.js';
import { triggerTenantWebhooks } from '../../integrations/n8n.js';
const log = createChildLogger('whatsapp:handler');
// مجلد مؤقت لحفظ الوسائط المحملة
const MEDIA_DIR = join(tmpdir(), 'muassis-media');
// ─── تحليل الرسالة الواردة ──────────────────────────────────────────────────
/**
 * يحول رسالة واتساب الخام إلى شكل موحد
 */
function parseIncomingMessage(message, contact) {
    // (Round-7 fix) Validate WhatsApp timestamp. Baileys sometimes emits 0 or a
    // pathological future value (cross-clock skew between WhatsApp servers and
    // device). Clamp to "now" for nonsense values so downstream date math
    // doesn't end up with epoch-0 or year-5000 messages in the journal.
    const rawTs = Number(message.timestamp);
    const VALID_MIN = 946684800; // 2000-01-01
    const VALID_MAX = 4102444800; // 2100-01-01
    const tsSeconds = Number.isFinite(rawTs) && rawTs >= VALID_MIN && rawTs <= VALID_MAX
        ? rawTs
        : Math.floor(Date.now() / 1000);
    const parsed = {
        messageId: message.id,
        from: message.from,
        timestamp: new Date(tsSeconds * 1000),
        type: message.type,
        profileName: contact?.profile?.name,
        contextMessageId: message.context?.id,
    };
    switch (message.type) {
        case 'text':
            parsed.text = message.text?.body;
            break;
        case 'image':
            parsed.mediaId = message.image?.id;
            parsed.mimeType = message.image?.mime_type;
            parsed.caption = message.image?.caption;
            break;
        case 'audio':
            parsed.mediaId = message.audio?.id;
            parsed.mimeType = message.audio?.mime_type;
            break;
        case 'video':
            parsed.mediaId = message.video?.id;
            parsed.mimeType = message.video?.mime_type;
            parsed.caption = message.video?.caption;
            break;
        case 'document':
            parsed.mediaId = message.document?.id;
            parsed.mimeType = message.document?.mime_type;
            parsed.caption = message.document?.caption;
            parsed.filename = message.document?.filename;
            break;
        case 'location':
            parsed.location = message.location;
            parsed.text = message.location
                ? `📍 ${message.location.name ?? ''} ${message.location.address ?? ''} (${message.location.latitude}, ${message.location.longitude})`.trim()
                : undefined;
            break;
        case 'contacts':
            parsed.contacts = message.contacts;
            parsed.text = message.contacts
                ?.map((c) => c.name.formatted_name)
                .join(', ');
            break;
        case 'interactive':
            if (message.interactive?.type === 'button_reply') {
                parsed.interactiveReply = {
                    type: 'button_reply',
                    id: message.interactive.button_reply.id,
                    title: message.interactive.button_reply.title,
                };
                parsed.text = message.interactive.button_reply.title;
            }
            else if (message.interactive?.type === 'list_reply') {
                parsed.interactiveReply = {
                    type: 'list_reply',
                    id: message.interactive.list_reply.id,
                    title: message.interactive.list_reply.title,
                    description: message.interactive.list_reply.description,
                };
                parsed.text = message.interactive.list_reply.title;
            }
            break;
        case 'button':
            parsed.buttonReply = {
                text: message.button?.text ?? '',
                payload: message.button?.payload ?? '',
            };
            parsed.text = message.button?.text;
            break;
        case 'sticker':
            parsed.mediaId = message.sticker?.id;
            parsed.mimeType = message.sticker?.mime_type;
            parsed.text = '[ملصق]';
            break;
        case 'reaction':
            // ردود الفعل لا تحتاج معالجة كاملة
            parsed.text = message.reaction?.emoji;
            break;
        default:
            parsed.text = `[نوع رسالة غير مدعوم: ${message.type}]`;
            break;
    }
    return parsed;
}
// ─── البحث عن المستأجر ──────────────────────────────────────────────────────
/**
 * يبحث عن المستأجر (tenant) بناءً على رقم هاتف الواتساب المستقبل
 */
async function findTenantByPhoneNumberId(phoneNumberId) {
    const { data, error } = await supabase
        .from('tenants')
        .select('id, name, settings')
        .eq('whatsapp_phone_number_id', phoneNumberId)
        .eq('is_active', true)
        .single();
    if (error || !data) {
        log.warn({ phoneNumberId, error }, 'Tenant not found for phone number ID');
        return null;
    }
    return data;
}
// ─── البحث عن / إنشاء العميل ────────────────────────────────────────────────
/**
 * يبحث عن سجل العميل أو ينشئ واحداً جديداً
 */
async function findOrCreateCustomer(tenantId, phone, profileName) {
    // محاولة البحث أولاً
    const { data: existing } = await supabase
        .from('customers')
        .select('id, phone, name')
        .eq('tenant_id', tenantId)
        .eq('phone', phone)
        .single();
    if (existing) {
        // تحديث الاسم إذا تغير
        if (profileName && existing.name !== profileName) {
            await supabase
                .from('customers')
                .update({ name: profileName, updated_at: new Date().toISOString() })
                .eq('id', existing.id);
            existing.name = profileName;
        }
        return existing;
    }
    // إنشاء عميل جديد
    const { data: newCustomer, error } = await supabase
        .from('customers')
        .insert({
        id: randomUUID(),
        tenant_id: tenantId,
        phone,
        name: profileName ?? null,
        channel: 'whatsapp',
        created_at: new Date().toISOString(),
    })
        .select('id, phone, name')
        .single();
    if (error || !newCustomer) {
        log.error({ tenantId, phone, error }, 'Failed to create customer');
        throw new Error(`Failed to create customer record: ${error?.message}`);
    }
    log.info({ customerId: newCustomer.id, phone }, 'New customer created');
    return newCustomer;
}
// ─── حفظ الوسائط ────────────────────────────────────────────────────────────
/**
 * يحمل ملف وسائط ويحفظه محلياً
 * يرجع المسار المحلي للملف
 */
async function downloadAndSaveMedia(client, mediaId, mimeType) {
    await mkdir(MEDIA_DIR, { recursive: true });
    const media = await client.downloadMedia(mediaId);
    // تحديد الامتداد من نوع MIME
    const ext = mimeExtension(mimeType);
    const filename = `${randomUUID()}${ext}`;
    const filePath = join(MEDIA_DIR, filename);
    await writeFile(filePath, media.buffer);
    log.debug({ filePath, mimeType, size: media.buffer.length }, 'Media saved to disk');
    return filePath;
}
function mimeExtension(mimeType) {
    const map = {
        'image/jpeg': '.jpg',
        'image/png': '.png',
        'image/webp': '.webp',
        'audio/ogg': '.ogg',
        'audio/mpeg': '.mp3',
        'audio/amr': '.amr',
        'audio/aac': '.aac',
        'video/mp4': '.mp4',
        'application/pdf': '.pdf',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
    };
    return map[mimeType] ?? '.bin';
}
// ─── تسجيل الرسالة في قاعدة البيانات ────────────────────────────────────────
async function logMessage(params) {
    try {
        await supabase.from('messages').insert({
            id: randomUUID(),
            tenant_id: params.tenantId,
            customer_id: params.customerId,
            wa_message_id: params.messageId,
            direction: params.direction,
            channel: params.channel,
            type: params.type,
            content: params.content,
            media_url: params.mediaUrl ?? null,
            metadata: params.metadata ?? null,
            created_at: new Date().toISOString(),
        });
    }
    catch (error) {
        // لا نريد أن يفشل المعالج بسبب خطأ في التسجيل
        log.error({ error, messageId: params.messageId }, 'Failed to log message');
    }
}
// ─── معالجة تحديثات الحالة ──────────────────────────────────────────────────
async function handleStatusUpdate(status) {
    log.debug({
        messageId: status.id,
        status: status.status,
        recipientId: status.recipient_id,
    }, 'Status update received');
    try {
        await supabase
            .from('messages')
            .update({
            delivery_status: status.status,
            status_updated_at: new Date().toISOString(),
        })
            .eq('wa_message_id', status.id);
    }
    catch (error) {
        log.warn({ error, messageId: status.id }, 'Failed to update message status');
    }
}
// ─── المعالج الرئيسي ────────────────────────────────────────────────────────
/**
 * معالج رسائل واتساب الرئيسي
 * يستقبل الـ webhook payload ويعالج كل رسالة
 */
export async function handleWhatsAppWebhook(payload) {
    for (const entry of payload.entry) {
        for (const change of entry.changes) {
            if (change.field !== 'messages')
                continue;
            const value = change.value;
            const phoneNumberId = value.metadata.phone_number_id;
            // معالجة تحديثات الحالة
            if (value.statuses) {
                await Promise.allSettled(value.statuses.map((status) => handleStatusUpdate(status)));
            }
            // معالجة الأخطاء
            if (value.errors) {
                for (const error of value.errors) {
                    log.error({ error }, 'WhatsApp webhook error');
                }
            }
            // معالجة الرسائل
            if (value.messages) {
                for (const message of value.messages) {
                    const contact = value.contacts?.find((c) => c.wa_id === message.from);
                    await processMessage(message, contact, phoneNumberId);
                }
            }
        }
    }
}
/**
 * معالجة رسالة واحدة
 */
async function processMessage(rawMessage, contact, phoneNumberId) {
    const parsed = parseIncomingMessage(rawMessage, contact);
    log.info({
        messageId: parsed.messageId,
        from: parsed.from,
        type: parsed.type,
        profileName: parsed.profileName,
    }, 'Processing incoming message');
    // الخطوة 1: تعليم الرسالة كمقروءة + إظهار "يكتب الآن…" (fire & forget)
    // Combined call so the customer sees ✓✓ AND typing... within ~1s.
    // Typing auto-clears on our outbound reply; if the agent takes >25s it
    // will silently fade — at that point the customer is already invested.
    whatsappClient.markAsReadAndShowTyping(parsed.messageId).catch(() => { });
    // تجاهل ردود الفعل — لا تحتاج رد
    if (parsed.type === 'reaction') {
        log.debug({ messageId: parsed.messageId }, 'Ignoring reaction message');
        return;
    }
    try {
        // الخطوة 2: تحديد المستأجر
        const tenant = await findTenantByPhoneNumberId(phoneNumberId);
        if (!tenant) {
            log.warn({ phoneNumberId, from: parsed.from }, 'No tenant configured for this phone number');
            // يمكن إرسال رسالة خطأ أو تجاهل
            return;
        }
        // الخطوة 3: تحديد / إنشاء العميل
        const customer = await findOrCreateCustomer(tenant.id, parsed.from, parsed.profileName);
        // الخطوة 4: تسجيل الرسالة الواردة
        await logMessage({
            tenantId: tenant.id,
            customerId: customer.id,
            messageId: parsed.messageId,
            direction: 'inbound',
            channel: 'whatsapp',
            type: parsed.type,
            content: parsed.text ?? parsed.caption ?? null,
            metadata: {
                profileName: parsed.profileName,
                contextMessageId: parsed.contextMessageId,
                interactiveReply: parsed.interactiveReply,
            },
        });
        // الخطوة 5: معالجة الوسائط حسب النوع
        let mediaPath;
        let messageForBrain = parsed.text ?? '';
        let extractedMediaText;
        let mediaTypeForBrain = 'text';
        if (parsed.type === 'audio' && parsed.mediaId && parsed.mimeType) {
            // رسالة صوتية → تحميل ثم تفريغ صوتي (Whisper)
            mediaPath = await downloadAndSaveMedia(whatsappClient, parsed.mediaId, parsed.mimeType);
            log.info({ mediaPath }, 'Voice message downloaded, transcribing...');
            const mediaResult = await processIncomingMedia('audio', mediaPath);
            extractedMediaText = mediaResult.processedContent;
            messageForBrain = extractedMediaText;
            mediaTypeForBrain = 'audio';
            log.info({ transcriptionLength: extractedMediaText.length }, 'Voice transcription complete');
        }
        else if (parsed.type === 'image' && parsed.mediaId && parsed.mimeType) {
            // صورة → تحميل ثم تحليل بصري (vision)
            mediaPath = await downloadAndSaveMedia(whatsappClient, parsed.mediaId, parsed.mimeType);
            log.info({ mediaPath }, 'Image downloaded, analyzing...');
            const mediaResult = await processIncomingMedia('image', mediaPath, parsed.caption);
            extractedMediaText = mediaResult.processedContent;
            messageForBrain = parsed.caption
                ? `${parsed.caption}\n\n[وصف الصورة]: ${extractedMediaText}`
                : extractedMediaText;
            mediaTypeForBrain = 'image';
        }
        else if (parsed.type === 'document' && parsed.mediaId && parsed.mimeType) {
            // مستند → تحميل ثم تحليل
            mediaPath = await downloadAndSaveMedia(whatsappClient, parsed.mediaId, parsed.mimeType);
            log.info({ mediaPath, filename: parsed.filename }, 'Document downloaded, analyzing...');
            const mediaResult = await processIncomingMedia('document', mediaPath);
            extractedMediaText = mediaResult.processedContent;
            messageForBrain = parsed.caption
                ? `${parsed.caption}\n\n[تحليل المستند: ${parsed.filename ?? 'file'}]: ${extractedMediaText}`
                : `[مستند: ${parsed.filename ?? 'file'}]\n${extractedMediaText}`;
            mediaTypeForBrain = 'document';
        }
        else if (parsed.type === 'video' && parsed.mediaId && parsed.mimeType) {
            mediaPath = await downloadAndSaveMedia(whatsappClient, parsed.mediaId, parsed.mimeType);
            const mediaResult = await processIncomingMedia('video', mediaPath);
            extractedMediaText = mediaResult.processedContent;
            messageForBrain = `[فيديو]: ${extractedMediaText}`;
            mediaTypeForBrain = 'video';
        }
        else if (parsed.type === 'sticker') {
            messageForBrain = '[ملصق]';
            mediaTypeForBrain = 'sticker';
        }
        // الخطوة 6: جلب آخر الرسائل للسياق
        const { data: recentMsgs } = await supabase
            .from('messages')
            .select('direction, content')
            .eq('tenant_id', tenant.id)
            .eq('customer_id', customer.id)
            .order('created_at', { ascending: false })
            .limit(10);
        const recentMessages = (recentMsgs ?? [])
            .reverse()
            .filter((m) => m.content)
            .map((m) => ({
            role: (m.direction === 'inbound' ? 'user' : 'assistant'),
            content: m.content,
        }));
        // الخطوة 7: إرسال إلى دماغ الوكيل (Agent Brain)
        const brainResponse = await brainProcessMessage(tenant.id, customer.id, messageForBrain, {
            mediaType: mediaTypeForBrain,
            extractedText: extractedMediaText,
            mediaUrl: mediaPath,
            recentMessages,
            // (Round-7 fix) Always supply a non-empty name to prevent downstream
            // entity-extraction code from passing undefined → KG row with NULL
            // Person name → broken recall.
            customerName: customer.name || parsed.profileName || `Customer ${customer.id.slice(0, 8)}`,
            customerPhone: parsed.from,
        });
        const responseText = brainResponse.response;
        // الخطوة 8: تحضير وسائط صادرة إذا لزم الأمر
        let sentResult;
        // هل يقترح الرد إرسال صوت؟
        const shouldSendVoice = responseText.includes('[VOICE_REPLY]') ||
            (mediaTypeForBrain === 'audio' && responseText.length < 500);
        // هل يقترح الرد إرسال PDF؟
        const shouldSendPDF = responseText.includes('[PDF_REPORT]') ||
            responseText.includes('[تقرير_PDF]');
        if (shouldSendVoice) {
            const cleanText = responseText.replace(/\[VOICE_REPLY\]/g, '').trim();
            try {
                const voiceResult = await prepareOutgoingMedia('voice', cleanText);
                if (voiceResult.mediaPath) {
                    // إرسال الصوت + النص كنسخة احتياطية
                    sentResult = await whatsappClient.sendAudio(parsed.from, voiceResult.mediaPath);
                    // أيضاً إرسال نسخة نصية
                    await whatsappClient.sendText(parsed.from, cleanText);
                }
                else {
                    sentResult = await whatsappClient.sendText(parsed.from, cleanText);
                }
            }
            catch (voiceErr) {
                log.warn({ error: voiceErr }, 'Voice synthesis failed, sending text only');
                sentResult = await whatsappClient.sendText(parsed.from, cleanText);
            }
        }
        else if (shouldSendPDF) {
            const cleanText = responseText.replace(/\[PDF_REPORT\]/g, '').replace(/\[تقرير_PDF\]/g, '').trim();
            try {
                const pdfResult = await prepareOutgoingMedia('pdf', cleanText);
                if (pdfResult.mediaPath) {
                    sentResult = await whatsappClient.sendDocument(parsed.from, pdfResult.mediaPath, `تقرير-${new Date().toISOString().slice(0, 10)}.pdf`, 'تقرير من المؤسس 📊');
                }
                else {
                    sentResult = await whatsappClient.sendText(parsed.from, cleanText);
                }
            }
            catch (pdfErr) {
                log.warn({ error: pdfErr }, 'PDF generation failed, sending text only');
                sentResult = await whatsappClient.sendText(parsed.from, cleanText);
            }
        }
        else {
            sentResult = await whatsappClient.sendText(parsed.from, responseText);
        }
        const sentMessageId = sentResult.messages?.[0]?.id;
        // الخطوة 9: تسجيل الرسالة الصادرة
        if (sentMessageId) {
            await logMessage({
                tenantId: tenant.id,
                customerId: customer.id,
                messageId: sentMessageId,
                direction: 'outbound',
                channel: 'whatsapp',
                type: 'text',
                content: responseText,
            });
        }
        // الخطوة 10: التعلم من المحادثة (C5: نتظر raw stub، LLM extraction async)
        await learnFromConversation(tenant.id, customer.id, customer.name || parsed.profileName || parsed.from || `Customer ${customer.id.slice(0, 8)}`, `العميل: ${messageForBrain}\nالمؤسس: ${responseText}`).catch((err) => log.debug({ err: err.message }, 'WhatsApp learn-from-conversation outer failed'));
        // الخطوة 11: تتبع الأنالتكس
        trackMessage(tenant.id, 'outbound', mediaTypeForBrain === 'text' ? 'text' : mediaTypeForBrain, brainResponse.model_used, brainResponse.tokens.input, brainResponse.tokens.output, brainResponse.cost, brainResponse.latencyMs).catch((err) => {
            log.warn({ error: err }, 'Analytics tracking failed (non-critical)');
        });
        // الخطوة 12: إطلاق webhooks لـ n8n حسب النية
        if (brainResponse.intent.intent === 'complaint') {
            triggerTenantWebhooks(tenant.id, 'complaint', {
                customerId: customer.id,
                customerName: customer.name,
                customerPhone: parsed.from,
                message: messageForBrain,
                response: responseText,
            }).catch(() => { });
        }
        log.info({
            messageId: parsed.messageId,
            customerId: customer.id,
            tenantId: tenant.id,
            responseMessageId: sentMessageId,
            intent: brainResponse.intent.intent,
            model: brainResponse.model_used,
            latencyMs: brainResponse.latencyMs,
        }, 'Message processed and response sent');
    }
    catch (error) {
        log.error({
            messageId: parsed.messageId,
            from: parsed.from,
            error,
        }, 'Failed to process message');
        // محاولة إرسال رسالة خطأ للعميل
        try {
            await whatsappClient.sendText(parsed.from, 'عذراً، حدث خطأ أثناء معالجة رسالتك. يرجى المحاولة مرة أخرى. 🙏');
        }
        catch (sendError) {
            log.error({ sendError, from: parsed.from }, 'Failed to send error message');
        }
    }
}
//# sourceMappingURL=handler.js.map