<?php
// whatsapp-bot/bot.php - WhatsApp Bot Webhook Handler (wifi2fm.com API)
// This file handles incoming webhooks from the wifi2fm.com WhatsApp API platform.
//
// FIXED: 2026-02-24 — Corrected payload parsing to match wifi2fm's actual
// webhook structure (nested data.data for baileys, data.message for received_message).

declare(strict_types=1);

// ─────────────────────────────────────────────────────────────
// Configuration
// ─────────────────────────────────────────────────────────────

function loadBotConfig(): array
{
    $configPath = __DIR__ . '/bot.config.json';
    if (!is_file($configPath)) {
        throw new RuntimeException("Missing bot config: {$configPath}");
    }

    $contents = file_get_contents($configPath);
    if ($contents === false) {
        throw new RuntimeException("Failed to read bot config: {$configPath}");
    }

    $decoded = json_decode($contents, true);
    if (!is_array($decoded)) {
        throw new RuntimeException("Invalid bot config JSON: {$configPath}");
    }

    return $decoded;
}

function validateBotConfig(array $config): void
{
    $instanceId = trim((string) ($config['instance_id'] ?? ''));
    $accessToken = trim((string) ($config['access_token'] ?? ''));

    if ($instanceId === '' || $accessToken === '') {
        throw new RuntimeException('Missing wifi2fm credentials: set instance_id and access_token in whatsapp-bot/bot.config.json');
    }
}

function validateIncomingWebhookSecret(array $config, array $payload): bool
{
    $expectedSecret = trim((string) ($config['webhook_secret'] ?? ''));
    if ($expectedSecret === '') {
        return true;
    }

    $headerSecret = trim((string) ($_SERVER['HTTP_X_WEBHOOK_SECRET'] ?? $_SERVER['HTTP_X_WIFI2FM_WEBHOOK_SECRET'] ?? ''));
    $querySecret = trim((string) ($_GET['secret'] ?? ''));
    $payloadSecret = trim((string) ($payload['webhook_secret'] ?? $payload['secret'] ?? ''));

    foreach ([$headerSecret, $querySecret, $payloadSecret] as $providedSecret) {
        if ($providedSecret !== '' && hash_equals($expectedSecret, $providedSecret)) {
            return true;
        }
    }

    logMessage('Webhook rejected: invalid/missing webhook secret');
    return false;
}

function toWebhookBool(mixed $value): bool
{
    if (is_bool($value)) {
        return $value;
    }

    if (is_int($value) || is_float($value)) {
        return (int) $value !== 0;
    }

    if (is_string($value)) {
        $normalized = strtolower(trim($value));
        if (in_array($normalized, ['1', 'true', 'yes', 'on'], true)) {
            return true;
        }

        if (in_array($normalized, ['0', 'false', 'no', 'off', ''], true)) {
            return false;
        }
    }

    return (bool) $value;
}

function extractBaileysText(array $messageNode): string
{
    if (isset($messageNode['conversation']) && is_string($messageNode['conversation'])) {
        return $messageNode['conversation'];
    }

    if (isset($messageNode['extendedTextMessage']['text']) && is_string($messageNode['extendedTextMessage']['text'])) {
        return $messageNode['extendedTextMessage']['text'];
    }

    if (isset($messageNode['imageMessage']['caption']) && is_string($messageNode['imageMessage']['caption'])) {
        return $messageNode['imageMessage']['caption'];
    }

    if (isset($messageNode['videoMessage']['caption']) && is_string($messageNode['videoMessage']['caption'])) {
        return $messageNode['videoMessage']['caption'];
    }

    return '';
}

/**
 * Check if a JID is a group chat (should be ignored by the bot).
 */
function isGroupJid(string $jid): bool
{
    return str_contains($jid, '@g.us') || str_contains($jid, '@broadcast') || str_contains($jid, '@newsletter');
}

function logMessageUpdateEvent(array $payload): bool
{
    $data = (array) ($payload['data'] ?? []);
    $eventType = (string) ($payload['event_type'] ?? $payload['event'] ?? $payload['type']
        ?? $data['event'] ?? $data['event_type'] ?? '');
    if (!in_array($eventType, ['messages.update', 'message.update'], true)) {
        return false;
    }

    // Try to find messages in nested structure too
    $innerData = (array) ($data['data'] ?? $data);
    $messages = (array) ($innerData['messages'] ?? []);
    $first = (array) (($messages[0] ?? []));
    $key = (array) ($first['key'] ?? []);
    $update = (array) ($first['update'] ?? []);

    $remoteJid = (string) ($key['remoteJid'] ?? $first['remoteJid'] ?? '');
    $msgId = (string) ($key['id'] ?? $first['id'] ?? '');
    $status = (string) ($update['status'] ?? $first['status'] ?? $data['status'] ?? 'unknown');

    logMessage('Message update event: type=' . $eventType . ' status=' . $status . ' msg_id=' . $msgId . ' remote_jid=' . $remoteJid);
    return true;
}

/**
 * Prevent duplicate processing of the same inbound message event.
 *
 * wifi2fm can emit multiple webhook events for one user action (for example,
 * messages.upsert + received_message). Sometimes these events have different
 * message IDs, so we deduplicate by both:
 * 1) message id (strong key)
 * 2) short-lived content fingerprint (fallback key)
 */
function shouldSkipDuplicateInbound(array $normalized): bool
{
    $dedupDir = __DIR__ . '/storage/dedup';
    if (!is_dir($dedupDir)) {
        @mkdir($dedupDir, 0775, true);
    }

    $keys = [];

    $msgId = trim((string) ($normalized['id'] ?? ''));
    if ($msgId !== '') {
        $keys[] = 'id:' . $msgId;
    }

    $sender = trim((string) ($normalized['sender_phone'] ?? ''));
    $type = trim((string) ($normalized['msg_type'] ?? 'chat'));
    $body = trim((string) ($normalized['body'] ?? ''));
    $caption = trim((string) ($normalized['caption'] ?? ''));

    // Content fallback key catches same user step delivered by multiple events.
    if ($sender !== '' || $body !== '' || $caption !== '') {
        $keys[] = 'fp:' . md5(strtolower($sender . '|' . $type . '|' . $body . '|' . $caption));
    }

    foreach ($keys as $key) {
        $dedupFile = $dedupDir . '/' . md5($key) . '.lock';
        if (is_file($dedupFile)) {
            $lockAge = time() - (int) @filemtime($dedupFile);
            // Keep a short window to avoid double replies for the same step.
            if ($lockAge < 30) {
                logMessage('[webhook] Dedup: skipping duplicate inbound key=' . $key . ' age=' . $lockAge . 's');
                return true;
            }
        }
    }

    foreach ($keys as $key) {
        $dedupFile = $dedupDir . '/' . md5($key) . '.lock';
        @file_put_contents($dedupFile, (string) time());
    }

    // Cleanup old dedup files (older than 60 seconds)
    foreach (@glob($dedupDir . '/*.lock') ?: [] as $file) {
        if (time() - (int) @filemtime($file) > 60) {
            @unlink($file);
        }
    }

    return false;
}

/**
 * FIXED: Main payload normalizer — handles wifi2fm's actual webhook structures.
 *
 * wifi2fm sends 3 types of payloads:
 *
 * 1. messages.upsert (baileys):
 *    { "data": { "event": "messages.upsert", "data": { "messages": [...], "type": "notify" } } }
 *    Note: messages array is in data.data.messages (double-nested!)
 *
 * 2. received_message (wifi2fm classic):
 *    { "data": { "event": "received_message", "message": { "body_message": {...}, "message_key": {...}, "push_name": "..." } } }
 *    Note: message data is in data.message (NOT data.from/data.body!)
 *
 * 3. Other events (contacts.update, connection.update, etc.) — ignored.
 */
function normalizeIncomingWebhookPayload(array $payload): ?array
{
    $data = (array) ($payload['data'] ?? []);

    // wifi2fm puts event type in: data.event (most common), or top-level event_type/event/type
    $eventType = (string) ($data['event'] ?? $data['event_type']
        ?? $payload['event_type'] ?? $payload['event'] ?? $payload['type'] ?? '');

    logMessage('[normalize] Detected event_type=' . ($eventType ?: '(empty)')
        . ' top_keys=' . implode(',', array_keys($payload))
        . ' data_keys=' . implode(',', array_keys($data)));

    // Skip non-message events
    if (in_array($eventType, [
        'test', 'ping', 'connection', 'connection.update',
        'contacts.update', 'contacts.upsert',
        'groups.update', 'group-participants.update',
        'chats.update', 'chats.upsert',
        'presence.update',
    ], true) || !empty($payload['test'])) {
        logMessage('[normalize] Skipping non-message event: ' . $eventType);
        return null;
    }

    // ── Format 1: messages.upsert (baileys) ──────────────────
    if (in_array($eventType, ['messages.upsert', 'message.upsert'], true)) {
        // wifi2fm nests: data.data.messages[] — the inner "data" contains the baileys payload
        $innerData = (array) ($data['data'] ?? []);

        // If inner data has messages, use it; otherwise fall back to data directly
        if (isset($innerData['messages']) && is_array($innerData['messages'])) {
            logMessage('[normalize] Using baileys format: data.data.messages[] (double-nested)');
            return parseBaileysUpsert($eventType, $innerData);
        }

        // Fallback: maybe messages is directly in data (older wifi2fm versions)
        if (isset($data['messages']) && is_array($data['messages'])) {
            logMessage('[normalize] Using baileys format: data.messages[] (single-nested)');
            return parseBaileysUpsert($eventType, $data);
        }

        logMessage('[normalize] messages.upsert: no messages array found in data or data.data');
        return null;
    }

    // ── Format 2: received_message (wifi2fm classic) ─────────
    if (in_array($eventType, ['received_message', 'webhook_message_received', 'message_received'], true)) {
        logMessage('[normalize] Using received_message format parser');

        // wifi2fm puts message details in data.message
        $messageObj = (array) ($data['message'] ?? []);

        if ($messageObj !== []) {
            return parseReceivedMessage($eventType, $messageObj);
        }

        // Fallback: old format where data directly has from/body
        if (isset($data['from']) && is_string($data['from']) && $data['from'] !== '') {
            logMessage('[normalize] received_message: using legacy data.from format');
            return parseLegacyClassicFormat($eventType, $data);
        }

        logMessage('[normalize] received_message: no message data found');
        return null;
    }

    // ── Fallback: auto-detect by payload shape ───────────────

    // Shape A: data.data.messages[] (baileys without event label)
    $innerData = (array) ($data['data'] ?? []);
    if (isset($innerData['messages']) && is_array($innerData['messages']) && !empty($innerData['messages'])) {
        logMessage('[normalize] Auto-detected baileys data.data.messages[] (event was "' . $eventType . '")');
        return parseBaileysUpsert($eventType ?: 'messages.upsert', $innerData);
    }

    // Shape B: data.messages[] (baileys without nesting)
    if (isset($data['messages']) && is_array($data['messages']) && !empty($data['messages'])) {
        logMessage('[normalize] Auto-detected baileys data.messages[] (event was "' . $eventType . '")');
        return parseBaileysUpsert($eventType ?: 'messages.upsert', $data);
    }

    // Shape C: data.message object (received_message without event label)
    if (isset($data['message']) && is_array($data['message'])) {
        $messageObj = (array) $data['message'];
        if (isset($messageObj['message_key']) || isset($messageObj['body_message'])) {
            logMessage('[normalize] Auto-detected received_message via data.message (event was "' . $eventType . '")');
            return parseReceivedMessage($eventType ?: 'received_message', $messageObj);
        }
    }

    // Shape D: data.from exists (legacy classic format)
    if (isset($data['from']) && is_string($data['from']) && $data['from'] !== '') {
        logMessage('[normalize] Auto-detected classic format via data.from (event was "' . $eventType . '")');
        return parseLegacyClassicFormat($eventType ?: 'message_received', $data);
    }

    // Shape E: top-level key/message structure (raw baileys without data wrapper)
    if (isset($payload['key']['remoteJid']) || isset($payload['message'])) {
        logMessage('[normalize] Auto-detected raw baileys structure at top level');
        $key = (array) ($payload['key'] ?? []);
        $messageNode = (array) ($payload['message'] ?? []);
        $remoteJid = (string) ($key['remoteJid'] ?? '');
        $senderPhone = str_replace(['@c.us', '@s.whatsapp.net'], '', $remoteJid);

        $msgType = 'chat';
        $imageUrl = '';
        $imageBase64 = '';
        if (isset($messageNode['imageMessage'])) {
            $msgType = 'image';
            $imageMessage = (array) $messageNode['imageMessage'];
            $imageUrl = (string) ($imageMessage['url'] ?? '');
            $imageBase64 = (string) ($imageMessage['jpegThumbnail'] ?? '');
        } elseif (isset($messageNode['documentMessage'])) {
            $msgType = 'document';
        }

        return [
            'event_type' => $eventType ?: 'messages.upsert',
            'from_me' => toWebhookBool($key['fromMe'] ?? false),
            'sender_phone' => $senderPhone,
            'remote_jid' => $remoteJid,
            'id' => (string) ($key['id'] ?? ''),
            'msg_type' => $msgType,
            'body' => extractBaileysText($messageNode),
            'caption' => '',
            'image_url' => $imageUrl,
            'image_base64' => $imageBase64,
            'contact_name' => (string) ($payload['pushName'] ?? ''),
        ];
    }

    logMessage('[normalize] No matching event type or payload shape, returning null');
    return null;
}

/**
 * Parse baileys messages.upsert format.
 * Expects $data to contain: { "messages": [...], "type": "notify" }
 */
function parseBaileysUpsert(string $eventType, array $data): ?array
{
    $messages = (array) ($data['messages'] ?? []);
    $first = (array) ($messages[0] ?? []);
    $key = (array) ($first['key'] ?? []);
    $messageNode = (array) ($first['message'] ?? []);

    if ($first === [] || $key === []) {
        logMessage('[normalize] Baileys format: empty message or key, returning null');
        return null;
    }

    $remoteJid = (string) ($key['remoteJid'] ?? '');
    $senderPhone = str_replace(['@c.us', '@s.whatsapp.net'], '', $remoteJid);

    // For group messages, use participant as the sender phone
    $participant = (string) ($key['participant'] ?? $key['participantAlt'] ?? '');
    if ($participant !== '' && isGroupJid($remoteJid)) {
        $senderPhone = str_replace(['@c.us', '@s.whatsapp.net'], '', $participant);
    }

    logMessage('[normalize] Baileys format: jid=' . $remoteJid . ' phone=' . $senderPhone . ' fromMe=' . var_export($key['fromMe'] ?? 'unset', true));

    $msgType = 'chat';
    $imageUrl = '';
    $imageBase64 = '';
    if (isset($messageNode['imageMessage'])) {
        $msgType = 'image';
        $imageMessage = (array) $messageNode['imageMessage'];
        $imageUrl = (string) ($imageMessage['url'] ?? '');
        $imageBase64 = (string) ($imageMessage['jpegThumbnail'] ?? '');
    } elseif (isset($messageNode['documentMessage'])) {
        $msgType = 'document';
    }

    return [
        'event_type' => $eventType,
        'from_me' => toWebhookBool($key['fromMe'] ?? false),
        'sender_phone' => $senderPhone,
        'remote_jid' => $remoteJid,
        'id' => (string) ($key['id'] ?? ''),
        'msg_type' => $msgType,
        'body' => extractBaileysText($messageNode),
        'caption' => '',
        'image_url' => $imageUrl,
        'image_base64' => $imageBase64,
        'contact_name' => (string) ($first['pushName'] ?? $data['pushName'] ?? ''),
    ];
}

/**
 * FIXED: Parse wifi2fm received_message format.
 *
 * Structure:
 * {
 *   "body_message": { "content": "...", "type": "textMessage", "messages": {...} },
 *   "message_key": { "remoteJid": "...", "fromMe": false, "id": "...", "participant": "..." },
 *   "push_name": "...",
 *   "from_contact": "..."
 * }
 */
function parseReceivedMessage(string $eventType, array $messageObj): ?array
{
    $bodyMessage = (array) ($messageObj['body_message'] ?? []);
    $messageKey = (array) ($messageObj['message_key'] ?? []);
    $pushName = (string) ($messageObj['push_name'] ?? $messageObj['pushName'] ?? '');

    $remoteJid = (string) ($messageKey['remoteJid'] ?? '');
    $senderPhone = str_replace(['@c.us', '@s.whatsapp.net'], '', $remoteJid);

    // For group messages, use participant as the sender phone
    $participant = (string) ($messageKey['participant'] ?? $messageKey['participantAlt'] ?? '');
    if ($participant !== '' && isGroupJid($remoteJid)) {
        $senderPhone = str_replace(['@c.us', '@s.whatsapp.net'], '', $participant);
    }

    // If sender phone is still a group ID, try from_contact
    if (isGroupJid($senderPhone . '@g.us') || $senderPhone === '') {
        $fromContact = (string) ($messageObj['from_contact'] ?? '');
        if ($fromContact !== '' && !isGroupJid($fromContact . '@g.us')) {
            $senderPhone = str_replace(['@c.us', '@s.whatsapp.net'], '', $fromContact);
        }
    }

    $content = (string) ($bodyMessage['content'] ?? '');
    $msgTypeRaw = strtolower((string) ($bodyMessage['type'] ?? 'textMessage'));

    logMessage('[normalize] received_message: jid=' . $remoteJid . ' phone=' . $senderPhone . ' type=' . $msgTypeRaw . ' content=' . mb_substr($content, 0, 80));

    // Determine message type
    $msgType = 'chat';
    $imageUrl = '';
    $imageBase64 = '';

    if (in_array($msgTypeRaw, ['imagemessage', 'image'], true)) {
        $msgType = 'image';
        $innerMessages = (array) ($bodyMessage['messages'] ?? []);
        $imgMsg = (array) ($innerMessages['imageMessage'] ?? []);
        $imageUrl = (string) ($imgMsg['url'] ?? '');
        $imageBase64 = (string) ($imgMsg['jpegThumbnail'] ?? '');
        if ($content === '' && isset($imgMsg['caption'])) {
            $content = (string) $imgMsg['caption'];
        }
    } elseif (in_array($msgTypeRaw, ['documentmessage', 'document'], true)) {
        $msgType = 'document';
    }

    if ($senderPhone === '') {
        logMessage('[normalize] received_message: could not extract sender phone, returning null');
        return null;
    }

    return [
        'event_type' => $eventType,
        'from_me' => toWebhookBool($messageKey['fromMe'] ?? false),
        'sender_phone' => $senderPhone,
        'remote_jid' => $remoteJid,
        'id' => (string) ($messageKey['id'] ?? ''),
        'msg_type' => $msgType,
        'body' => $content,
        'caption' => '',
        'image_url' => $imageUrl,
        'image_base64' => $imageBase64,
        'contact_name' => $pushName,
    ];
}

/**
 * Parse legacy classic format where data directly has from/body fields.
 */
function parseLegacyClassicFormat(string $eventType, array $data): ?array
{
    $fromRaw = (string) ($data['from'] ?? '');
    $senderPhone = str_replace(['@c.us', '@s.whatsapp.net'], '', $fromRaw);
    logMessage('[normalize] Legacy classic: from=' . $fromRaw . ' phone=' . $senderPhone);

    return [
        'event_type' => $eventType,
        'from_me' => toWebhookBool($data['fromMe'] ?? false),
        'sender_phone' => $senderPhone,
        'remote_jid' => $fromRaw,
        'id' => (string) ($data['id'] ?? ''),
        'msg_type' => (string) ($data['type'] ?? 'chat'),
        'body' => (string) ($data['body'] ?? ''),
        'caption' => (string) ($data['caption'] ?? ''),
        'image_url' => (string) ($data['body'] ?? ''),
        'image_base64' => (string) ($data['media'] ?? ''),
        'contact_name' => (string) ($data['pushName'] ?? $data['notifyName'] ?? ''),
    ];
}

// ─────────────────────────────────────────────────────────────
// Logging
// ─────────────────────────────────────────────────────────────

function logMessage(string $message): void
{
    $timestamp = date('Y-m-d H:i:s');
    $line = "[{$timestamp}] {$message}\n";
    @file_put_contents(__DIR__ . '/storage/whatsapp_bot.log', $line, FILE_APPEND);
    if (PHP_SAPI === 'cli') {
        echo $line;
    }
}


function paymentProofDebugEnabled(array $config): bool
{
    return toWebhookBool($config['payment_proof_ai_debug'] ?? false);
}

function logPaymentProofDebug(array $config, array $entry): void
{
    if (!paymentProofDebugEnabled($config)) {
        return;
    }

    $path = trim((string) ($config['payment_proof_ai_debug_file'] ?? ''));
    if ($path === '') {
        $path = __DIR__ . '/storage/payment_proof_ai_debug.jsonl';
    } elseif (!str_starts_with($path, '/')) {
        $path = __DIR__ . '/' . ltrim($path, '/');
    }

    $dir = dirname($path);
    if (!is_dir($dir)) {
        @mkdir($dir, 0775, true);
    }

    $payload = $entry;
    $payload['ts'] = date('c');
    $line = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    if (is_string($line)) {
        @file_put_contents($path, $line . "\n", FILE_APPEND);
    }
}

// ─────────────────────────────────────────────────────────────
// wifi2fm.com WhatsApp API Helpers
// ─────────────────────────────────────────────────────────────

function whatsappApiSend(array $config, array $params): array
{
    if (!function_exists('curl_init')) {
        logMessage('[api-send] ERROR: cURL extension not available');
        return ['status' => 'error', 'message' => 'cURL extension is not available'];
    }

    $baseUrl = rtrim((string) ($config['api_base_url'] ?? 'https://wifi2fm.com'), '/');
    $url = "{$baseUrl}/api/send";

    $params['instance_id'] = (string) ($config['instance_id'] ?? '');
    $params['access_token'] = (string) ($config['access_token'] ?? '');

    $logParams = $params;
    unset($logParams['access_token']);
    logMessage('[api-send] POST ' . $url . ' params=' . json_encode($logParams, JSON_UNESCAPED_UNICODE));

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/x-www-form-urlencoded',
        ],
        CURLOPT_POSTFIELDS => http_build_query($params),
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlError = curl_error($ch);
    curl_close($ch);

    if ($response === false) {
        logMessage('[api-send] FAILED: curl_error=' . ($curlError ?: 'WhatsApp API unreachable'));
        return ['status' => 'error', 'message' => $curlError ?: 'WhatsApp API unreachable'];
    }

    logMessage('[api-send] Response HTTP ' . $httpCode . ': ' . mb_substr((string) $response, 0, 500));

    $decoded = json_decode($response, true);
    if (!is_array($decoded)) {
        logMessage('[api-send] ERROR: Invalid JSON response');
        return ['status' => 'error', 'message' => 'Invalid response', 'http_code' => $httpCode, 'raw' => $response];
    }

    $apiStatus = (string) ($decoded['status'] ?? 'unknown');
    logMessage('[api-send] Result: status=' . $apiStatus);

    return $decoded;
}

function sendWhatsAppTextMessage(array $config, string $recipientPhone, string $text): void
{
    logMessage('[send-text] Sending to=' . $recipientPhone . ' text=' . mb_substr($text, 0, 200));

    $result = whatsappApiSend($config, [
        'number' => $recipientPhone,
        'type' => 'text',
        'message' => $text,
    ]);

    if (($result['status'] ?? '') === 'error') {
        logMessage('[send-text] FAILED: ' . json_encode($result));
    } else {
        logMessage('[send-text] OK: message sent to ' . $recipientPhone);
    }
}

function sendWhatsAppImageMessage(array $config, string $recipientPhone, string $imageUrl, string $caption = ''): void
{
    $params = [
        'number' => $recipientPhone,
        'type' => 'media',
        'media_url' => $imageUrl,
    ];

    if ($caption !== '') {
        $params['message'] = $caption;
    }

    $result = whatsappApiSend($config, $params);

    if (($result['status'] ?? '') === 'error') {
        logMessage('sendImageMessage failed: ' . json_encode($result));
    }
}

function downloadMediaFromUrl(string $url): ?string
{
    if ($url === '') {
        return null;
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_MAXREDIRS => 5,
    ]);
    $data = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($data === false || $httpCode !== 200) {
        logMessage('[upload-debug] downloadMediaFromUrl: failed, http_code=' . $httpCode . ' url=' . $url);
        return null;
    }

    logMessage('[upload-debug] downloadMediaFromUrl: success, size=' . strlen($data) . ' bytes');
    return $data;
}


function downloadFullImage($arg1, $arg2 = 0, $arg3 = null, $arg4 = null): ?string
{
    // Backward/forward compatible wrapper for older call signatures that may pass
    // ($config, $messageId, $imageUrl, $imageBase64) or ($url, $expectedSize).
    $url = '';
    $expectedSize = 0;

    if (is_string($arg1) && $arg1 !== '') {
        $url = $arg1;
        $expectedSize = is_numeric($arg2) ? (int) $arg2 : 0;
    } elseif (is_array($arg1)) {
        $url = is_string($arg3) ? $arg3 : '';
        $expectedSize = is_numeric($arg2) ? (int) $arg2 : 0;

        if ($expectedSize <= 0 && is_string($arg4) && $arg4 !== '') {
            $decoded = base64_decode($arg4, true);
            if ($decoded !== false) {
                $expectedSize = strlen($decoded);
            }
        }
    }

    if ($url === '') {
        return null;
    }

    return downloadMediaFromUrl($url);
}

function saveImageToFile(string $imageData, string $uploadDir, string $publicBasePath): ?string
{
    if ($imageData === '') {
        return null;
    }

    if (!is_dir($uploadDir) && !@mkdir($uploadDir, 0775, true) && !is_dir($uploadDir)) {
        return null;
    }

    if (!is_writable($uploadDir)) {
        return null;
    }

    $filename = uuidV4() . '.jpg';
    $fullPath = rtrim($uploadDir, '/\\') . DIRECTORY_SEPARATOR . $filename;
    $written = @file_put_contents($fullPath, $imageData);
    if ($written === false) {
        return null;
    }

    return rtrim($publicBasePath, '/') . '/' . $filename;
}


function verifyPaymentProofWithChatGpt(array $config, string $imageData, array $context = []): array
{
    $debugMeta = [
        'event' => 'payment_proof_verification',
        'image_size' => strlen($imageData),
        'image_sha256' => $imageData !== '' ? hash('sha256', $imageData) : '',
        'expected_amount' => (string) ($context['amount'] ?? ''),
        'expected_channel' => (string) ($context['payment_channel'] ?? ''),
        'expected_provider' => (string) ($context['payment_provider_name'] ?? ''),
    ];

    $enabled = toWebhookBool($config['payment_proof_ai_enabled'] ?? false);
    if (!$enabled) {
        logPaymentProofDebug($config, $debugMeta + ['status' => 'skipped', 'reason' => 'ai_check_disabled']);
        return ['ok' => true, 'reason' => 'ai_check_disabled'];
    }

    if ($imageData === '') {
        logPaymentProofDebug($config, $debugMeta + ['status' => 'rejected', 'reason' => 'empty_image']);
        return ['ok' => false, 'reason' => 'empty_image'];
    }

    $apiKey = trim((string) ($config['openai_api_key'] ?? getenv('OPENAI_API_KEY') ?: ''));
    if ($apiKey === '') {
        logMessage('[payment-proof-ai] blocked: missing OPENAI API key');
        logPaymentProofDebug($config, $debugMeta + ['status' => 'error', 'reason' => 'missing_openai_api_key']);
        return ['ok' => false, 'reason' => 'missing_openai_api_key'];
    }

    if (!function_exists('curl_init')) {
        logMessage('[payment-proof-ai] blocked: cURL extension missing');
        logPaymentProofDebug($config, $debugMeta + ['status' => 'error', 'reason' => 'curl_unavailable']);
        return ['ok' => false, 'reason' => 'curl_unavailable'];
    }

    $model = trim((string) ($config['payment_proof_ai_model'] ?? 'gpt-4.1-mini'));
    $expectedAmount = (string) ($context['amount'] ?? '');
    $expectedChannel = (string) ($context['payment_channel'] ?? '');
    $expectedProvider = (string) ($context['payment_provider_name'] ?? '');

    $systemPrompt = 'You are a strict payment-proof verifier for WhatsApp transfer receipts. Reject anything that does not look like a real payment proof screenshot/document. Always return JSON only.';
    $userPrompt = 'Validate this image as payment proof. Expected amount: ' . $expectedAmount
        . ', channel: ' . $expectedChannel
        . ', provider: ' . $expectedProvider
        . '. Mark invalid if it is unrelated image, blurry, fake-looking edit, or missing core receipt details (amount/date/reference/account/provider).';

    $schema = [
        'type' => 'object',
        'additionalProperties' => false,
        'properties' => [
            'is_valid' => ['type' => 'boolean'],
            'reason' => ['type' => 'string'],
            'confidence' => ['type' => 'number'],
        ],
        'required' => ['is_valid', 'reason', 'confidence'],
    ];

    $payload = [
        'model' => $model,
        'input' => [
            [
                'role' => 'system',
                'content' => [
                    ['type' => 'input_text', 'text' => $systemPrompt],
                ],
            ],
            [
                'role' => 'user',
                'content' => [
                    ['type' => 'input_text', 'text' => $userPrompt],
                    ['type' => 'input_image', 'image_url' => 'data:image/jpeg;base64,' . base64_encode($imageData)],
                ],
            ],
        ],
        'text' => [
            'format' => [
                'type' => 'json_schema',
                'name' => 'payment_proof_validation',
                'schema' => $schema,
                'strict' => true,
            ],
        ],
        'max_output_tokens' => 180,
    ];

    $ch = curl_init('https://api.openai.com/v1/responses');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 45,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $apiKey,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
    ]);

    $raw = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlErr = curl_error($ch);
    curl_close($ch);

    if ($raw === false || $httpCode < 200 || $httpCode >= 300) {
        logMessage('[payment-proof-ai] blocked: OpenAI call failed http=' . $httpCode . ' err=' . $curlErr);
        logPaymentProofDebug($config, $debugMeta + ['status' => 'error', 'reason' => 'openai_request_failed', 'http_code' => $httpCode]);
        return ['ok' => false, 'reason' => 'openai_request_failed'];
    }

    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        logMessage('[payment-proof-ai] blocked: invalid OpenAI JSON response');
        logPaymentProofDebug($config, $debugMeta + ['status' => 'error', 'reason' => 'invalid_openai_response']);
        return ['ok' => false, 'reason' => 'invalid_openai_response'];
    }

    $outputText = (string) ($decoded['output_text'] ?? '');
    if ($outputText === '') {
        $outputText = (string) (($decoded['output'][0]['content'][0]['text'] ?? ''));
    }

    $result = json_decode($outputText, true);
    if (!is_array($result) || !array_key_exists('is_valid', $result)) {
        logMessage('[payment-proof-ai] blocked: unable to parse model decision');
        logPaymentProofDebug($config, $debugMeta + ['status' => 'error', 'reason' => 'unparseable_model_output']);
        return ['ok' => false, 'reason' => 'unparseable_model_output'];
    }

    $isValid = (bool) ($result['is_valid'] ?? false);
    $reason = trim((string) ($result['reason'] ?? ''));
    $confidence = (float) ($result['confidence'] ?? 0.0);

    logMessage('[payment-proof-ai] decision valid=' . ($isValid ? 'yes' : 'no') . ' confidence=' . $confidence . ' reason=' . mb_substr($reason, 0, 180));

    if (!$isValid) {
        $finalReason = $reason !== '' ? $reason : 'invalid_payment_proof';
        logPaymentProofDebug($config, $debugMeta + ['status' => 'rejected', 'reason' => $finalReason, 'confidence' => $confidence]);
        return ['ok' => false, 'reason' => $finalReason];
    }

    logPaymentProofDebug($config, $debugMeta + ['status' => 'accepted', 'reason' => 'valid', 'confidence' => $confidence]);
    return ['ok' => true, 'reason' => 'valid', 'confidence' => $confidence];
}

// ─────────────────────────────────────────────────────────────
// Database
// ─────────────────────────────────────────────────────────────

function waDb(array $config): PDO
{
    static $pdo = null;
    if ($pdo instanceof PDO) {
        return $pdo;
    }

    $host = (string) ($config['db_host'] ?? '127.0.0.1');
    $port = (string) ($config['db_port'] ?? '3306');
    $name = (string) ($config['db_name'] ?? '');
    $user = (string) ($config['db_user'] ?? 'root');
    $pass = (string) ($config['db_pass'] ?? '');

    if ($name === '') {
        throw new RuntimeException('db_name is missing in bot.config.json');
    }

    $pdo = new PDO(
        "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4",
        $user,
        $pass,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]
    );

    return $pdo;
}

function ensureFlowSchema(PDO $pdo): void
{
    static $ensured = false;
    if ($ensured) {
        return;
    }

    $pdo->exec("CREATE TABLE IF NOT EXISTS `bot_conversations` (
        `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        `bot_id` BIGINT UNSIGNED NOT NULL,
        `chat_id` VARCHAR(40) NOT NULL,
        `state` VARCHAR(80) NOT NULL,
        `context_json` JSON NULL,
        `last_message_at` DATETIME NOT NULL,
        `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
        `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (`id`),
        UNIQUE KEY `uq_bot_chat` (`bot_id`, `chat_id`)
    ) ENGINE=InnoDB");

    ensureColumnExists($pdo, 'users', 'mobile_number', 'VARCHAR(20) NULL');
    ensureColumnExists($pdo, 'transections', 'payment_channel', "ENUM('wallet','bank') NOT NULL DEFAULT 'wallet'");
    ensureColumnExists($pdo, 'transections', 'payment_provider_name', "VARCHAR(120) NOT NULL DEFAULT ''");
    ensureColumnExists($pdo, 'transections', 'image_data', "LONGBLOB NULL");
    ensureBinaryColumnType($pdo, 'transections', 'image_data', 'LONGBLOB');
    ensureColumnExists($pdo, 'transections', 'transaction_type', "VARCHAR(20) NULL");
    ensureColumnExists($pdo, 'transections', 'account_name', "VARCHAR(255) NULL");
    ensureColumnExists($pdo, 'transections', 'submit_via', "ENUM('telegram','whatsapp','none') NOT NULL DEFAULT 'none'");
    ensureColumnExists($pdo, 'transections', 'submitter_telegram_id', 'VARCHAR(40) NULL');
    ensureColumnExists($pdo, 'transections', 'submitter_whatsapp_id', 'VARCHAR(40) NULL');

    $ensured = true;
}

function ensureBinaryColumnType(PDO $pdo, string $table, string $column, string $binaryType = 'LONGBLOB'): void
{
    $stmt = $pdo->prepare('SELECT DATA_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1');
    $stmt->execute([$table, $column]);
    $dataType = strtolower((string) ($stmt->fetchColumn() ?: ''));

    if ($dataType === '') {
        return;
    }

    if (!in_array($dataType, ['blob', 'mediumblob', 'longblob'], true)) {
        $pdo->exec("ALTER TABLE `{$table}` MODIFY COLUMN `{$column}` {$binaryType} NULL");
    }
}

function ensureColumnExists(PDO $pdo, string $table, string $column, string $ddl): void
{
    try {
        $stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?');
        $stmt->execute([$table, $column]);
        if ((int) $stmt->fetchColumn() === 0) {
            $pdo->exec("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$ddl}");
        }
    } catch (Throwable $e) {
        // Silently ignore "column already exists" errors (error code 1060)
        if (str_contains($e->getMessage(), '1060') || str_contains($e->getMessage(), 'Duplicate column')) {
            return;
        }
        throw $e;
    }
}

function ensureSupportTicketsTable(PDO $pdo): void
{
    static $ensured = false;
    if ($ensured) {
        return;
    }

    $pdo->exec("CREATE TABLE IF NOT EXISTS `support_tickets` (
        `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        `ticket_uid` CHAR(36) NOT NULL,
        `manager_id` BIGINT UNSIGNED NOT NULL,
        `user_id` BIGINT UNSIGNED NULL,
        `channel` ENUM('telegram','whatsapp','web') NOT NULL DEFAULT 'whatsapp',
        `subject` VARCHAR(150) NOT NULL DEFAULT 'شكوى',
        `message` TEXT NOT NULL,
        `status` ENUM('open','in_progress','closed') NOT NULL DEFAULT 'open',
        `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
        `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (`id`),
        UNIQUE KEY `uq_support_tickets_ticket_uid` (`ticket_uid`),
        KEY `idx_support_tickets_manager_id` (`manager_id`),
        KEY `idx_support_tickets_user_id` (`user_id`),
        KEY `idx_support_tickets_status` (`status`)
    ) ENGINE=InnoDB");

    $ensured = true;
}

function ensureSubscriptionRequestsTable(PDO $pdo): void
{
    static $ensured = false;
    if ($ensured) {
        return;
    }

    $pdo->exec("CREATE TABLE IF NOT EXISTS `subscription_requests` (
        `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        `request_uid` CHAR(36) NOT NULL,
        `manager_id` BIGINT UNSIGNED NOT NULL,
        `telegram_id` VARCHAR(40) NOT NULL DEFAULT '',
        `whatsapp_id` VARCHAR(40) NOT NULL DEFAULT '',
        `subscriber_full_name` VARCHAR(255) NOT NULL DEFAULT '',
        `subscriber_id_number` VARCHAR(20) NOT NULL DEFAULT '',
        `id_document_image_url` VARCHAR(500) NOT NULL DEFAULT '',
        `contact_mobile` VARCHAR(20) NOT NULL,
        `exact_location` VARCHAR(255) NOT NULL,
        `nearest_landmark` VARCHAR(255) NOT NULL,
        `channel` ENUM('telegram','whatsapp','web') NOT NULL DEFAULT 'whatsapp',
        `status` ENUM('open','in_progress','closed') NOT NULL DEFAULT 'open',
        `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
        `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (`id`),
        UNIQUE KEY `uq_subscription_requests_uid` (`request_uid`),
        KEY `idx_subscription_requests_manager_id` (`manager_id`),
        KEY `idx_subscription_requests_status` (`status`)
    ) ENGINE=InnoDB");

    ensureColumnExists($pdo, 'subscription_requests', 'whatsapp_id', "VARCHAR(40) NOT NULL DEFAULT ''");
    ensureColumnExists($pdo, 'subscription_requests', 'id_document_image_data', 'LONGBLOB NULL');

    $ensured = true;
}

// ─────────────────────────────────────────────────────────────
// Conversation State Management
// ─────────────────────────────────────────────────────────────

function getConversation(PDO $pdo, int $botId, string $chatId): ?array
{
    $stmt = $pdo->prepare('SELECT `state`, `context_json` FROM `bot_conversations` WHERE `bot_id` = ? AND `chat_id` = ? LIMIT 1');
    $stmt->execute([$botId, $chatId]);
    $row = $stmt->fetch();
    if (!$row) {
        return null;
    }

    $context = json_decode((string) ($row['context_json'] ?? '{}'), true);

    return [
        'state' => (string) ($row['state'] ?? ''),
        'context' => is_array($context) ? $context : [],
    ];
}

function saveConversation(PDO $pdo, int $botId, string $chatId, string $state, array $context): void
{
    $stmt = $pdo->prepare('INSERT INTO `bot_conversations` (`bot_id`, `chat_id`, `state`, `context_json`, `last_message_at`) VALUES (?, ?, ?, ?, NOW())
        ON DUPLICATE KEY UPDATE `state` = VALUES(`state`), `context_json` = VALUES(`context_json`), `last_message_at` = NOW()');
    $stmt->execute([$botId, $chatId, $state, json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)]);
}

function clearConversation(PDO $pdo, int $botId, string $chatId): void
{
    $stmt = $pdo->prepare('DELETE FROM `bot_conversations` WHERE `bot_id` = ? AND `chat_id` = ?');
    $stmt->execute([$botId, $chatId]);
}

// ─────────────────────────────────────────────────────────────
// Bot Context & User Identity
// ─────────────────────────────────────────────────────────────

function findBotContext(PDO $pdo, array $config): array
{
    static $cache = null;
    if (is_array($cache)) {
        return $cache;
    }

    $instanceId = (string) ($config['instance_id'] ?? '');
    logMessage('[bot-context] Looking up bot for instance_id=' . $instanceId);

    $stmt = $pdo->prepare('SELECT `id`, `manager_id` FROM `bots` WHERE `platform` = ? AND `bot_token` = ? LIMIT 1');
    $stmt->execute(['whatsapp', $instanceId]);
    $row = $stmt->fetch();

    if (!$row) {
        logMessage('[bot-context] Bot not found in DB, will auto-create');
        $configuredManagerId = (int) ($config['manager_id'] ?? 0);
        if ($configuredManagerId <= 0) {
            $managerRow = $pdo->query('SELECT `id` FROM `managers` ORDER BY `id` ASC LIMIT 1')->fetch();
            $configuredManagerId = (int) ($managerRow['id'] ?? 0);
        }

        if ($configuredManagerId <= 0) {
            throw new RuntimeException('Cannot resolve bot context: no manager is available.');
        }

        logMessage('Warning: WhatsApp bot not found in bots table; creating record for manager_id=' . $configuredManagerId);

        $botName = 'WhatsAppBot-' . substr(sha1($instanceId), 0, 8);
        $webhookSecret = bin2hex(random_bytes(32));

        try {
            $stmtInsert = $pdo->prepare('INSERT INTO `bots` (`manager_id`, `bot_token`, `bot_name`, `platform`, `webhook_secret`, `status`) VALUES (?, ?, ?, ?, ?, ?)');
            $stmtInsert->execute([$configuredManagerId, $instanceId, $botName, 'whatsapp', $webhookSecret, 'active']);
        } catch (Throwable $e) {
            logMessage('Warning: auto-create bot row skipped: ' . $e->getMessage());
        }

        $stmtReload = $pdo->prepare('SELECT `id`, `manager_id` FROM `bots` WHERE `platform` = ? AND `bot_token` = ? LIMIT 1');
        $stmtReload->execute(['whatsapp', $instanceId]);
        $row = $stmtReload->fetch();

        if (!$row) {
            throw new RuntimeException('Cannot resolve bot context after auto-create attempt.');
        }
    }

    $cache = [
        'bot_id' => (int) $row['id'],
        'manager_id' => (int) $row['manager_id'],
        'brand_name' => fetchGeneralBrandName($pdo, (int) $row['manager_id']),
    ];

    return $cache;
}

function fetchGeneralBrandName(PDO $pdo, int $managerId = 0): string
{
    try {
        $stmt = $pdo->query('SELECT `brand_name` FROM `general_config` WHERE `id` = 1 LIMIT 1');
        $brandName = trim((string) ($stmt?->fetchColumn() ?: ''));
        if ($brandName !== '') {
            return $brandName;
        }
    } catch (Throwable) {
    }

    if ($managerId > 0) {
        $stmt = $pdo->prepare('SELECT `brand_name` FROM `managers` WHERE `id` = ? LIMIT 1');
        $stmt->execute([$managerId]);
        return trim((string) ($stmt->fetchColumn() ?: ''));
    }

    return '';
}

function buildWhatsAppUserIdentity(string $senderPhone, string $contactName): array
{
    $displayName = $contactName !== '' ? $contactName : $senderPhone;

    return [
        'display_name' => $displayName,
        'whatsapp_id' => $senderPhone,
        'phone' => $senderPhone,
    ];
}

// ─────────────────────────────────────────────────────────────
// Shared Helpers
// ─────────────────────────────────────────────────────────────

function normalizeMobile(string $mobile): ?string
{
    $digits = preg_replace('/\D+/', '', $mobile);
    if (!is_string($digits) || strlen($digits) < 9 || strlen($digits) > 15) {
        return null;
    }
    if (str_starts_with($digits, '970')) {
        return '+' . $digits;
    }
    if (str_starts_with($digits, '0')) {
        return $digits;
    }
    return '0' . $digits;
}

function palestinianBanks(): array
{
    return [
        'بنك فلسطين',
        'البنك العربي',
        'بنك القدس',
        'بنك الإسكان',
        'البنك الإسلامي الفلسطيني',
        'البنك الوطني',
        'بنك الاستثمار الفلسطيني',
        'بنك صفا',
        'بنك الأردن',
        'بنك القاهرة عمان',
    ];
}

function uuidV4(): string
{
    $data = random_bytes(16);
    $data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
    $data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}

function canonicalizeText(string $text): string
{
    $text = mb_strtolower(trim($text));
    $text = preg_replace('/[^\p{Arabic}\p{L}\p{N}\/ ]+/u', ' ', $text) ?? $text;
    $text = preg_replace('/\s+/u', ' ', $text) ?? $text;
    return trim($text);
}

function isStartText(string $text): bool
{
    $c = canonicalizeText($text);
    return $c === '/start' || $c === 'مرحبا' || $c === 'ابدأ' || $c === 'start' || $c === 'hi' || $c === 'hello';
}

function isNewTransferText(string $text): bool
{
    $c = canonicalizeText($text);
    return $c === '/pay' || $c === 'تحويل جديد' || $c === 'تحويل' || $c === 'دفع';
}

function isNewSubscriptionRequestText(string $text): bool
{
    $c = canonicalizeText($text);
    return in_array($c, ['تقديم طلب اشتراك جديد', 'طلب اشتراك', 'اشتراك جديد'], true);
}

function isComplaintText(string $text): bool
{
    $c = canonicalizeText($text);
    return in_array($c, ['شكوى', 'تقديم شكوى'], true)
        || str_starts_with($c, 'شكوى ')
        || str_starts_with($c, 'تقديم شكوى ');
}

function isSupportText(string $text): bool
{
    $c = canonicalizeText($text);
    return in_array($c, ['الدعم الفني', 'دعم فني', 'دعم'], true);
}

function extractComplaintMessage(string $text): string
{
    $raw = trim($text);
    $normalized = canonicalizeText($raw);

    if ($normalized === 'شكوى' || $normalized === 'تقديم شكوى') {
        return '';
    }

    $raw = preg_replace('/^تقديم\s+شكوى\s*/u', '', $raw) ?? $raw;
    $raw = preg_replace('/^شكوى\s*/u', '', $raw) ?? $raw;

    return trim($raw);
}

// ─────────────────────────────────────────────────────────────
// Menu Helpers
// ─────────────────────────────────────────────────────────────

function buildNumberedMenu(string $headerText, array $options): string
{
    $lines = [$headerText, ''];
    foreach ($options as $i => $option) {
        $num = $i + 1;
        $title = (string) ($option['title'] ?? $option);
        $lines[] = "{$num}. {$title}";
    }
    $lines[] = '';
    $lines[] = 'أرسل الرقم للمتابعة 👆';
    return implode("\n", $lines);
}

function resolveNumberedReply(string $text, array $options): ?string
{
    $trimmed = trim($text);

    if (ctype_digit($trimmed)) {
        $index = (int) $trimmed - 1;
        if (isset($options[$index])) {
            return (string) ($options[$index]['title'] ?? $options[$index]);
        }
    }

    return null;
}

// ─────────────────────────────────────────────────────────────
// Support ticket helpers
// ─────────────────────────────────────────────────────────────

function hasOpenSupportTicketByWhatsAppId(PDO $pdo, int $managerId, string $whatsappId): bool
{
    if ($managerId <= 0 || trim($whatsappId) === '') {
        return false;
    }

    $stmt = $pdo->prepare("SELECT COUNT(*) FROM `support_tickets` st LEFT JOIN `users` u ON u.`id` = st.`user_id` WHERE st.`manager_id` = ? AND st.`status` IN ('open','in_progress') AND u.`whatsapp_number` = ?");
    $stmt->execute([$managerId, $whatsappId]);
    return (int) $stmt->fetchColumn() > 0;
}

function createSupportTicket(PDO $pdo, array $botContext, array $userIdentity, string $message, ?string $contactMobile = null): int
{
    ensureSupportTicketsTable($pdo);

    $managerId = (int) ($botContext['manager_id'] ?? 0);
    $whatsappId = (string) ($userIdentity['whatsapp_id'] ?? '');

    $userId = null;
    if ($whatsappId !== '' && $managerId > 0) {
        $stmt = $pdo->prepare('SELECT `id` FROM `users` WHERE `whatsapp_number` = ? AND `manager_id` = ? LIMIT 1');
        $stmt->execute([$whatsappId, $managerId]);
        $uid = $stmt->fetchColumn();
        if ($uid !== false) {
            $userId = (int) $uid;
        }
    }

    $fullMessage = trim($message);
    if ($contactMobile !== null && trim($contactMobile) !== '') {
        $fullMessage .= "\nرقم التواصل: " . trim($contactMobile);
    }

    $stmt = $pdo->prepare('INSERT INTO `support_tickets` (`ticket_uid`,`manager_id`,`user_id`,`channel`,`subject`,`message`,`status`) VALUES (?,?,?,?,?,?,?)');
    $stmt->execute([
        uuidV4(),
        $managerId,
        $userId,
        'whatsapp',
        'شكوى',
        $fullMessage,
        'open',
    ]);

    return (int) $pdo->lastInsertId();
}

function hasSubscriptionRequestByWhatsAppId(PDO $pdo, int $managerId, string $whatsappId): bool
{
    if ($managerId <= 0 || trim($whatsappId) === '') {
        return false;
    }

    ensureSubscriptionRequestsTable($pdo);
    $stmt = $pdo->prepare('SELECT COUNT(*) FROM `subscription_requests` WHERE `manager_id` = ? AND `whatsapp_id` = ?');
    $stmt->execute([$managerId, trim($whatsappId)]);
    return (int) $stmt->fetchColumn() > 0;
}

function createSubscriptionRequest(PDO $pdo, int $managerId, string $whatsappId, string $fullName, string $idNumber, string $idImageUrl, string $mobile, string $exactLocation, string $nearestLandmark, ?string $idImageData = null): int
{
    ensureSubscriptionRequestsTable($pdo);

    $stmt = $pdo->prepare('INSERT INTO `subscription_requests` (`request_uid`,`manager_id`,`whatsapp_id`,`subscriber_full_name`,`subscriber_id_number`,`id_document_image_url`,`id_document_image_data`,`contact_mobile`,`exact_location`,`nearest_landmark`,`channel`,`status`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)');
    $stmt->execute([
        uuidV4(),
        $managerId,
        trim($whatsappId),
        trim($fullName),
        trim($idNumber),
        trim($idImageUrl),
        $idImageData,
        trim($mobile),
        trim($exactLocation),
        trim($nearestLandmark),
        'whatsapp',
        'open',
    ]);

    return (int) $pdo->lastInsertId();
}

function storeSubscriptionIdImage(string $imageData): ?string
{
    $uploadDir = __DIR__ . '/../www/uploads/subscription_ids';

    if (!is_dir($uploadDir)) {
        $mkResult = @mkdir($uploadDir, 0775, true);
        if (!$mkResult && !is_dir($uploadDir)) {
            return null;
        }
    }

    if (!is_writable($uploadDir)) {
        return null;
    }

    $filename = date('YmdHis') . '-' . bin2hex(random_bytes(6)) . '.jpg';
    $path = $uploadDir . '/' . $filename;
    $written = @file_put_contents($path, $imageData);
    if (!is_int($written) || $written <= 0) {
        return null;
    }

    return '/uploads/subscription_ids/' . $filename;
}

// ─────────────────────────────────────────────────────────────
// Response Builder
// ─────────────────────────────────────────────────────────────

function sendBotResponse(array $config, string $recipientPhone, array $response, PDO $pdo, array $botContext): void
{
    $text = (string) ($response['text'] ?? '');
    $responseType = (string) ($response['type'] ?? 'text');
    $chatId = $recipientPhone;

    logMessage('[bot-response] Preparing response for ' . $recipientPhone . ' type=' . $responseType . ' text=' . mb_substr($text, 0, 150));

    switch ($responseType) {
        case 'interactive_buttons':
            $buttons = (array) ($response['buttons'] ?? []);
            $menuOptions = [];
            foreach ($buttons as $btn) {
                $menuOptions[] = ['title' => (string) ($btn['title'] ?? ''), 'id' => (string) ($btn['id'] ?? '')];
            }
            $menuText = buildNumberedMenu($text, $menuOptions);
            sendWhatsAppTextMessage($config, $recipientPhone, $menuText);

            $stateRow = getConversation($pdo, $botContext['bot_id'], $chatId);
            $currentState = $stateRow['state'] ?? null;
            $currentContext = $stateRow['context'] ?? [];
            $currentContext['_pending_menu'] = $menuOptions;
            if ($currentState !== null) {
                saveConversation($pdo, $botContext['bot_id'], $chatId, $currentState, $currentContext);
            } else {
                saveConversation($pdo, $botContext['bot_id'], $chatId, '_menu_pending', $currentContext);
            }
            break;

        case 'interactive_list':
            $sections = (array) ($response['sections'] ?? []);
            $menuOptions = [];
            foreach ($sections as $section) {
                foreach ((array) ($section['rows'] ?? []) as $row) {
                    $menuOptions[] = ['title' => (string) ($row['title'] ?? ''), 'id' => (string) ($row['id'] ?? '')];
                }
            }
            $menuText = buildNumberedMenu($text, $menuOptions);
            sendWhatsAppTextMessage($config, $recipientPhone, $menuText);

            $stateRow = getConversation($pdo, $botContext['bot_id'], $chatId);
            $currentState = $stateRow['state'] ?? null;
            $currentContext = $stateRow['context'] ?? [];
            $currentContext['_pending_menu'] = $menuOptions;
            if ($currentState !== null) {
                saveConversation($pdo, $botContext['bot_id'], $chatId, $currentState, $currentContext);
            } else {
                saveConversation($pdo, $botContext['bot_id'], $chatId, '_menu_pending', $currentContext);
            }
            break;

        default:
            sendWhatsAppTextMessage($config, $recipientPhone, $text);
            break;
    }
}

// ─────────────────────────────────────────────────────────────
// Main Update Handler
// ─────────────────────────────────────────────────────────────

function handleBotUpdate(PDO $pdo, array $botContext, array $config, array $messageData, array $userIdentity): array
{
    $chatId = (string) ($userIdentity['whatsapp_id'] ?? '');
    $text = trim((string) ($messageData['body'] ?? ''));

    logMessage('[handle-update] chat_id=' . $chatId . ' raw_text=' . mb_substr($text, 0, 100) . ' msg_type=' . (string) ($messageData['type'] ?? 'unknown'));

    $stateRow = getConversation($pdo, $botContext['bot_id'], $chatId);
    $state = $stateRow['state'] ?? null;
    $context = $stateRow['context'] ?? [];

    logMessage('[handle-update] Current state=' . ($state ?? 'NULL') . ' has_pending_menu=' . (isset($context['_pending_menu']) ? 'yes' : 'no'));

    // Resolve numbered menu reply if there's a pending menu
    if (isset($context['_pending_menu']) && is_array($context['_pending_menu'])) {
        $resolved = resolveNumberedReply($text, $context['_pending_menu']);
        if ($resolved !== null) {
            logMessage('[handle-update] Menu resolved: "' . $text . '" => "' . $resolved . '"');
            $text = $resolved;
        } else {
            logMessage('[handle-update] Menu not resolved for input: "' . $text . '"');
        }
        unset($context['_pending_menu']);

        if ($state === '_menu_pending') {
            clearConversation($pdo, $botContext['bot_id'], $chatId);
            $state = null;
        } else if ($state !== null) {
            saveConversation($pdo, $botContext['bot_id'], $chatId, $state, $context);
        }
    }

    // /start or greeting
    if (isStartText($text)) {
        logMessage('[handle-update] Matched: /start or greeting');
        clearConversation($pdo, $botContext['bot_id'], $chatId);
        $brandName = (string) ($botContext['brand_name'] ?? '');
        $brandLine = $brandName !== '' ? "\n🏦 {$brandName}\n" : '';

        $welcomeText = implode("\n", [
            'أهلاً وسهلاً بك! 👋',
            '',
            '🤖✨ انا مساعدك الذكي بنكي بوت، اقدر اساعدك في:',
            '💳 تسجيل اشعارات الدفع الخاصة بك',
            '🛠️ تقديم شكوى للشركة',
            '🆕 تسجيل طلب اشتراك في الخدمة',
            '',
            '💫 الزبون الكريم',
            $userIdentity['display_name'],
            $brandLine,
            'اختر من القائمة للمتابعة 👇',
        ]);

        return [
            'text' => $welcomeText,
            'type' => 'interactive_buttons',
            'buttons' => [
                ['id' => 'action_new_transfer', 'title' => '💸 تحويل جديد'],
                ['id' => 'action_support', 'title' => '🛠️ الدعم الفني'],
            ],
        ];
    }

    // Support menu
    if (isSupportText($text)) {
        logMessage('[handle-update] Matched: support text');
        return [
            'text' => '🛠️ اختر نوع الطلب:',
            'type' => 'interactive_buttons',
            'buttons' => [
                ['id' => 'action_complaint', 'title' => '📝 تقديم شكوى'],
                ['id' => 'action_subscription', 'title' => '🆕 طلب اشتراك'],
            ],
        ];
    }

    // Continue existing conversation state
    if ($state !== null && $state !== '_menu_pending') {
        logMessage('[handle-update] Continuing state flow: state=' . $state);
        return handleStateStep($pdo, $botContext, $config, $messageData, $userIdentity, $state, $context, $text);
    }

    // Complaint
    if (isComplaintText($text)) {
        logMessage('[handle-update] Matched: complaint text');
        if (hasOpenSupportTicketByWhatsAppId($pdo, (int) ($botContext['manager_id'] ?? 0), (string) ($userIdentity['whatsapp_id'] ?? ''))) {
            return ['text' => '⚠️ لديك شكوى مفتوحة حالياً. لا يمكن إضافة شكوى جديدة حتى يتم تحديث حالة التذكرة الحالية.'];
        }

        $complaintMessage = extractComplaintMessage($text);
        if ($complaintMessage === '') {
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'support_ticket_message', [
                'whatsapp_id' => $userIdentity['whatsapp_id'],
            ]);
            return ['text' => '📝 الرجاء كتابة الشكوى باختصار حتى يتم التواصل معك في أسرع وقت ممكن.'];
        }

        saveConversation($pdo, $botContext['bot_id'], $chatId, 'support_ticket_mobile', [
            'whatsapp_id' => $userIdentity['whatsapp_id'],
            'complaint_message' => $complaintMessage,
        ]);
        return ['text' => '📱 يرجى كتابة رقم الجوال للتواصل معك.'];
    }

    // Subscription request
    if (isNewSubscriptionRequestText($text)) {
        logMessage('[handle-update] Matched: subscription request text');
        if (hasSubscriptionRequestByWhatsAppId($pdo, (int) ($botContext['manager_id'] ?? 0), (string) ($userIdentity['whatsapp_id'] ?? ''))) {
            return ['text' => 'ℹ️ يوجد طلب اشتراك مسجل مسبقاً لهذا الحساب، وسيتم التواصل معك قريباً.'];
        }

        saveConversation($pdo, $botContext['bot_id'], $chatId, 'subscription_request_full_name', [
            'whatsapp_id' => $userIdentity['whatsapp_id'],
        ]);

        return ['text' => '👤 يرجى كتابة الاسم الكامل للمشترك الجديد.'];
    }

    // New transfer
    if (isNewTransferText($text)) {
        logMessage('[handle-update] Matched: new transfer text');
        $stmt = $pdo->prepare('SELECT * FROM `users` WHERE `whatsapp_number` = ? AND `manager_id` = ? LIMIT 1');
        $stmt->execute([$userIdentity['whatsapp_id'], $botContext['manager_id']]);
        $user = $stmt->fetch();

        if (!$user) {
            logMessage('[handle-update] User not found for whatsapp_id=' . $userIdentity['whatsapp_id'] . ', starting registration');
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'register_first_name', [
                'whatsapp_id' => $userIdentity['whatsapp_id'],
                'display_name' => $userIdentity['display_name'],
            ]);
            return ['text' => '📝 لا يوجد حساب لك حالياً. للتسجيل أرسل الاسم الأول.'];
        }

        saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_account_owner_choice', [
            'initiator_user_id' => (int) $user['id'],
            'initiator_whatsapp_id' => (string) $userIdentity['whatsapp_id'],
            'initiator_full_name' => trim(((string) ($user['first_name'] ?? '')) . ' ' . ((string) ($user['last_name'] ?? ''))),
            'initiator_mobile' => (string) ($user['mobile_number'] ?? $user['whatsapp_number'] ?? ''),
        ]);

        return [
            'text' => '💸 هل التحويل من حسابك او من حساب اخر',
            'type' => 'interactive_buttons',
            'buttons' => [
                ['id' => 'owner_personal', 'title' => '👤 من حسابي'],
                ['id' => 'owner_other', 'title' => '👥 من حساب اخر'],
            ],
        ];
    }

    // Default response
    logMessage('[handle-update] No match for text="' . mb_substr($text, 0, 100) . '", sending default menu');
    return [
        'text' => '🤖 لم أفهم طلبك. اختر من القائمة للمتابعة.',
        'type' => 'interactive_buttons',
        'buttons' => [
            ['id' => 'action_new_transfer', 'title' => '💸 تحويل جديد'],
            ['id' => 'action_support', 'title' => '🛠️ الدعم الفني'],
        ],
    ];
}

// ─────────────────────────────────────────────────────────────
// State Machine Handler
// ─────────────────────────────────────────────────────────────

function handleStateStep(PDO $pdo, array $botContext, array $config, array $messageData, array $userIdentity, string $state, array $context, string $text): array
{
    $chatId = (string) ($userIdentity['whatsapp_id'] ?? '');

    logMessage('[state-step] chat_id=' . $chatId . ' state=' . $state . ' text=' . mb_substr($text, 0, 100));

    switch ($state) {
        // ── Registration Flow ──────────────────────────────
        case 'register_first_name':
            if ($text === '') {
                return ['text' => '⚠️ أرسل الاسم الأول.'];
            }
            $context['first_name'] = $text;
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'register_last_name', $context);
            return ['text' => '🧾 ممتاز، أرسل اسم العائلة.'];

        case 'register_last_name':
            if ($text === '') {
                return ['text' => '⚠️ أرسل اسم العائلة.'];
            }
            $context['last_name'] = $text;
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'register_mobile', $context);
            return ['text' => '📱 أرسل رقم الجوال.'];

        case 'register_mobile':
            $mobile = normalizeMobile($text);
            if ($mobile === null) {
                return ['text' => '⚠️ رقم الجوال غير صحيح.'];
            }
            $stmt = $pdo->prepare('SELECT `id` FROM `users` WHERE `mobile_number` = ? AND `manager_id` = ? LIMIT 1');
            $stmt->execute([$mobile, $botContext['manager_id']]);
            if ($stmt->fetch()) {
                return ['text' => '⚠️ هذا الرقم مستخدم مسبقاً. أرسل رقم آخر.'];
            }

            $whatsappId = (string) ($context['whatsapp_id'] ?? $userIdentity['whatsapp_id']);
            $stmt = $pdo->prepare('INSERT INTO `users` (`telegram_id`,`whatsapp_number`,`mobile_number`,`first_name`,`last_name`,`manager_id`) VALUES (?,?,?,?,?,?)');
            $stmt->execute([
                0,
                $whatsappId,
                $mobile,
                (string) ($context['first_name'] ?? ''),
                (string) ($context['last_name'] ?? ''),
                $botContext['manager_id'],
            ]);
            $newUserId = (int) $pdo->lastInsertId();
            logMessage('[state-step] User registered: user_id=' . $newUserId . ' whatsapp_id=' . $whatsappId . ' mobile=' . $mobile);

            saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_account_owner_choice', [
                'initiator_user_id' => $newUserId,
                'initiator_whatsapp_id' => $whatsappId,
                'initiator_full_name' => trim(((string) ($context['first_name'] ?? '')) . ' ' . ((string) ($context['last_name'] ?? ''))),
                'initiator_mobile' => $mobile,
            ]);

            return [
                'text' => '✅ تم تسجيلك. الآن هل التحويل من حسابك او من حساب اخر',
                'type' => 'interactive_buttons',
                'buttons' => [
                    ['id' => 'owner_personal', 'title' => '👤 من حسابي'],
                    ['id' => 'owner_other', 'title' => '👥 من حساب اخر'],
                ],
            ];

        // ── Transaction Flow ───────────────────────────────
        case 'tx_account_owner_choice':
            $c = canonicalizeText($text);
            if (in_array($c, ['1', 'لحسابي', 'التحويل من حسابي', 'من حسابي'], true)) {
                $context['target_user_id'] = (int) ($context['initiator_user_id'] ?? 0);
                $context['paid_for_mobile'] = (string) ($context['initiator_mobile'] ?? '');
                $context['transaction_type'] = 'personal';
                saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_amount', $context);
                return ['text' => '💰 أرسل قيمة التحويل.'];
            }

            if (in_array($c, ['2', 'لحساب آخر', 'لحساب اخر', 'التحويل من حساب اخر', 'التحويل من حساب آخر', 'من حساب اخر'], true)) {
                $context['transaction_type'] = 'other';
                $initiatorWhatsAppId = (string) ($context['initiator_whatsapp_id'] ?? '');
                if ($initiatorWhatsAppId !== '') {
                    $userRow = $pdo->prepare('SELECT `first_name`, `last_name` FROM `users` WHERE `whatsapp_number` = ? AND `manager_id` = ? LIMIT 1');
                    $userRow->execute([$initiatorWhatsAppId, $botContext['manager_id']]);
                    $fetchedUser = $userRow->fetch();
                    if ($fetchedUser) {
                        $context['account_name'] = trim(((string) ($fetchedUser['first_name'] ?? '')) . ' ' . ((string) ($fetchedUser['last_name'] ?? '')));
                    }
                }
                saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_other_account_name', $context);
                return ['text' => '🧾 ارسل اسم صاحب الحساب'];
            }

            return [
                'text' => '⚠️ اختر من الخيارات:',
                'type' => 'interactive_buttons',
                'buttons' => [
                    ['id' => 'owner_personal', 'title' => '👤 من حسابي'],
                    ['id' => 'owner_other', 'title' => '👥 من حساب اخر'],
                ],
            ];

        case 'tx_other_account_name':
            $ownerName = trim($text);
            if ($ownerName === '') {
                return ['text' => '⚠️ ارسل اسم صاحب الحساب'];
            }
            $context['target_account_owner_name'] = $ownerName;
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_amount', $context);
            return ['text' => '💰 أرسل قيمة التحويل.'];

        case 'tx_amount':
            $amount = str_replace(',', '.', $text);
            if (!is_numeric($amount) || (float) $amount <= 0) {
                return ['text' => '⚠️ قيمة التحويل غير صحيحة.'];
            }
            $context['amount'] = number_format((float) $amount, 2, '.', '');
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_channel', $context);

            return [
                'text' => '🏦 اختر طريقة الدفع:',
                'type' => 'interactive_buttons',
                'buttons' => [
                    ['id' => 'channel_bank', 'title' => '🏦 بنك'],
                    ['id' => 'channel_wallet', 'title' => '👛 محفظة'],
                ],
            ];

        case 'tx_channel':
            $c = canonicalizeText($text);
            if (in_array($c, ['محفظة', '2'], true)) {
                $context['payment_channel'] = 'wallet';
                saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_wallet_name', $context);
                return [
                    'text' => '👛 اختر اسم المحفظة:',
                    'type' => 'interactive_buttons',
                    'buttons' => [
                        ['id' => 'wallet_jawwal', 'title' => '📲 محفظة جوال باي'],
                        ['id' => 'wallet_palpay', 'title' => '💳 محفظة بال باي'],
                    ],
                ];
            }
            if (in_array($c, ['بنك', '1'], true)) {
                $context['payment_channel'] = 'bank';
                saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_bank_name', $context);

                $menuOptions = [];
                foreach (palestinianBanks() as $bank) {
                    $menuOptions[] = ['title' => $bank, 'id' => 'bank_' . $bank];
                }

                return [
                    'text' => '🏦 اختر اسم البنك:',
                    'type' => 'interactive_list',
                    'button_label' => '🏦 البنوك',
                    'sections' => [
                        [
                            'title' => 'البنوك الفلسطينية',
                            'rows' => array_map(fn($i, $b) => ['id' => 'bank_' . $i, 'title' => $b], array_keys(palestinianBanks()), palestinianBanks()),
                        ],
                    ],
                ];
            }
            return [
                'text' => '⚠️ اختر بنك أو محفظة فقط.',
                'type' => 'interactive_buttons',
                'buttons' => [
                    ['id' => 'channel_bank', 'title' => '🏦 بنك'],
                    ['id' => 'channel_wallet', 'title' => '👛 محفظة'],
                ],
            ];

        case 'tx_wallet_name':
            $normalizedWallet = canonicalizeText($text);
            if (in_array($normalizedWallet, ['1', 'jawwal pay', 'محفظة جوال باي'], true)) {
                $context['payment_provider_name'] = 'محفظة جوال باي';
            } elseif (in_array($normalizedWallet, ['2', 'palpay', 'محفظة بال باي'], true)) {
                $context['payment_provider_name'] = 'محفظة بال باي';
            } else {
                return [
                    'text' => '⚠️ اختر محفظة جوال باي أو محفظة بال باي.',
                    'type' => 'interactive_buttons',
                    'buttons' => [
                        ['id' => 'wallet_jawwal', 'title' => '📲 محفظة جوال باي'],
                        ['id' => 'wallet_palpay', 'title' => '💳 محفظة بال باي'],
                    ],
                ];
            }
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_upload_image', $context);
            return ['text' => '🖼️ أرسل صورة إثبات الدفع.'];

        case 'tx_bank_name':
            $banks = palestinianBanks();
            $bank = null;

            $trimmedText = trim($text);
            if (ctype_digit($trimmedText)) {
                $bankIndex = (int) $trimmedText - 1;
                if (isset($banks[$bankIndex])) {
                    $bank = $banks[$bankIndex];
                }
            }

            if ($bank === null) {
                foreach ($banks as $b) {
                    if ($text === $b || canonicalizeText($text) === canonicalizeText($b)) {
                        $bank = $b;
                        break;
                    }
                }
            }

            if ($bank === null) {
                saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_bank_name', $context);
                return [
                    'text' => '⚠️ اختر بنكاً من القائمة.',
                    'type' => 'interactive_list',
                    'button_label' => '🏦 البنوك',
                    'sections' => [
                        [
                            'title' => 'البنوك الفلسطينية',
                            'rows' => array_map(fn($i, $b) => ['id' => 'bank_' . $i, 'title' => $b], array_keys($banks), $banks),
                        ],
                    ],
                ];
            }

            $context['payment_provider_name'] = $bank;
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'tx_upload_image', $context);
            return ['text' => '🖼️ أرسل صورة إثبات الدفع.'];

        case 'tx_upload_image':
            logMessage('[upload-debug] tx_upload_image: step entered for chat_id=' . $chatId);

            $imageUrl = (string) ($messageData['_image_url'] ?? '');
            $imageBase64 = (string) ($messageData['_image_base64'] ?? '');

            if ($imageUrl === '' && $imageBase64 === '') {
                logMessage('[upload-debug] tx_upload_image: no image in message');
                return ['text' => '⚠️ هذه الخطوة تحتاج صورة فقط. أرسل صورة إثبات الدفع.'];
            }

            $imageData = null;
            if ($imageUrl !== '') {
                // When URL exists, require full media download (avoid storing jpegThumbnail preview).
                $imageData = downloadMediaFromUrl($imageUrl);
                if ($imageData === null) {
                    logMessage('[upload-debug] tx_upload_image: full media URL download failed; refusing thumbnail fallback');
                }
            } elseif ($imageBase64 !== '') {
                $imageData = base64_decode($imageBase64, true) ?: null;
            }

            if ($imageData === null) {
                logMessage('[upload-debug] tx_upload_image: failed to get image data');
                return ['text' => '❌ تعذر تحميل الصورة. أعد المحاولة.'];
            }
            logMessage('[upload-debug] tx_upload_image: image obtained, size=' . strlen($imageData) . ' bytes');

            $proofValidation = verifyPaymentProofWithChatGpt($config, $imageData, $context);
            if (($proofValidation['ok'] ?? false) !== true) {
                $reason = (string) ($proofValidation['reason'] ?? 'invalid_payment_proof');
                logMessage('[upload-debug] tx_upload_image: rejected by AI validation, reason=' . $reason);
                return ['text' => '❌ صورة إثبات الدفع غير مقبولة. الرجاء إرسال صورة واضحة لإيصال تحويل صحيح.'];
            }

            $uploadDir = __DIR__ . '/../www/uploads/transections';
            $savedImageUrl = null;
            $imageBlobForDb = $imageData;

            if (!is_dir($uploadDir)) {
                @mkdir($uploadDir, 0775, true);
            }

            if (is_dir($uploadDir) && is_writable($uploadDir)) {
                $imageFilename = uuidV4() . '.jpg';
                $imageFilePath = $uploadDir . '/' . $imageFilename;

                $writeResult = @file_put_contents($imageFilePath, $imageData);
                if ($writeResult !== false) {
                    $savedImageUrl = '/uploads/transections/' . $imageFilename;
                    $imageBlobForDb = null;
                }
            }

            $transactionType = (string) ($context['transaction_type'] ?? 'personal');
            $defaultSenderName = (string) (($context['initiator_full_name'] ?? '') !== '' ? $context['initiator_full_name'] : $userIdentity['display_name']);
            $senderFullName = $transactionType === 'other'
                ? (string) ($context['target_account_owner_name'] ?? $defaultSenderName)
                : $defaultSenderName;
            $accountName = $transactionType === 'personal'
                ? $senderFullName
                : (string) ($context['account_name'] ?? '');

            try {
                $stmt = $pdo->prepare('INSERT INTO `transections` (`transaction_uid`,`manager_id`,`user_id`,`sender_full_name`,`amount`,`paid_for_mobile`,`payment_time`,`payment_type`,`payment_channel`,`payment_provider_name`,`approval_required_status`,`approved_status`,`note`,`image_url`,`image_data`,`transaction_type`,`account_name`,`submit_via`,`submitter_whatsapp_id`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)');
                $params = [
                    uuidV4(),
                    $botContext['manager_id'],
                    ($context['target_user_id'] ?? null) ?: null,
                    $senderFullName,
                    (string) ($context['amount'] ?? '0.00'),
                    (string) ($context['paid_for_mobile'] ?? ''),
                    date('Y-m-d H:i:s'),
                    'complete',
                    (string) ($context['payment_channel'] ?? 'wallet'),
                    (string) ($context['payment_provider_name'] ?? ''),
                    1,
                    0,
                    'Created via WhatsApp bot flow | Account Owner: ' . (string) ($context['target_account_owner_name'] ?? '') . ' | Submitter WhatsApp: ' . (string) ($context['initiator_whatsapp_id'] ?? ''),
                    $savedImageUrl,
                    $imageBlobForDb,
                    $transactionType,
                    $accountName,
                    'whatsapp',
                    (string) ($context['initiator_whatsapp_id'] ?? ''),
                ];
                $stmt->execute($params);
            } catch (\Throwable $e) {
                logMessage('[upload-debug] tx_upload_image: DB INSERT FAILED: ' . $e->getMessage());
                return ['text' => '❌ تعذر حفظ المعاملة في قاعدة البيانات. أعد المحاولة.'];
            }

            $tranId = (int) $pdo->lastInsertId();
            logMessage('[upload-debug] tx_upload_image: transaction saved, id=' . $tranId);
            clearConversation($pdo, $botContext['bot_id'], $chatId);

            return [
                'text' => "✅ تم استلام التحويل رقم #{$tranId} وسيتم مراجعته من المدير ⏳",
                'type' => 'interactive_buttons',
                'buttons' => [
                    ['id' => 'action_new_transfer', 'title' => '💸 تحويل جديد'],
                    ['id' => 'action_support', 'title' => '🛠️ الدعم الفني'],
                ],
            ];

        // ── Subscription Request Flow ──────────────────────
        case 'subscription_request_full_name':
            if ($text === '' || mb_strlen($text) < 5) {
                return ['text' => '⚠️ يرجى كتابة الاسم الكامل (ثنائي على الأقل).'];
            }

            $context['subscriber_full_name'] = $text;
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'subscription_request_id_number', $context);
            return ['text' => '🪪 يرجى إدخال رقم الهوية الفلسطينية (9 أرقام فقط).'];

        case 'subscription_request_id_number':
            $idNumber = preg_replace('/\D+/', '', $text);
            if (!is_string($idNumber) || strlen($idNumber) !== 9) {
                return ['text' => '⚠️ رقم الهوية يجب أن يكون 9 أرقام فقط.'];
            }

            $context['subscriber_id_number'] = $idNumber;
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'subscription_request_id_image', $context);
            return ['text' => '📸 الآن يرجى إرسال صورة واضحة للهوية الفلسطينية.'];

        case 'subscription_request_id_image':
            $imageUrl = (string) ($messageData['_image_url'] ?? '');
            $imageBase64 = (string) ($messageData['_image_base64'] ?? '');

            if ($imageUrl === '' && $imageBase64 === '') {
                return ['text' => '⚠️ يرجى إرسال صورة الهوية كصورة (وليس نص).'];
            }

            $imageData = null;
            if ($imageUrl !== '') {
                // When URL exists, require full media download (avoid storing jpegThumbnail preview).
                $imageData = downloadMediaFromUrl($imageUrl);
            } elseif ($imageBase64 !== '') {
                $imageData = base64_decode($imageBase64, true) ?: null;
            }

            if ($imageData === null) {
                return ['text' => '⚠️ تعذر تحميل صورة الهوية. حاول الإرسال مرة أخرى.'];
            }

            $savedIdImageUrl = storeSubscriptionIdImage($imageData);
            if ($savedIdImageUrl !== null) {
                $context['id_document_image_url'] = $savedIdImageUrl;
            } else {
                $context['id_document_image_url'] = '';
                $context['id_document_image_b64'] = base64_encode($imageData);
            }

            saveConversation($pdo, $botContext['bot_id'], $chatId, 'subscription_request_location', $context);
            return ['text' => '📍 يرجى كتابة العنوان بالتفصيل.'];

        case 'subscription_request_location':
            if ($text === '') {
                return ['text' => '⚠️ يرجى كتابة الموقع بالتفصيل.'];
            }

            $context['exact_location'] = $text;
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'subscription_request_landmark', $context);
            return ['text' => '🕌 يرجى كتابة أقرب مبنى أو مسجد معروف بالقرب منك.'];

        case 'subscription_request_landmark':
            if ($text === '') {
                return ['text' => '⚠️ يرجى كتابة أقرب مبنى أو مسجد.'];
            }

            if (hasSubscriptionRequestByWhatsAppId($pdo, (int) ($botContext['manager_id'] ?? 0), (string) ($userIdentity['whatsapp_id'] ?? ''))) {
                clearConversation($pdo, $botContext['bot_id'], $chatId);
                return ['text' => 'ℹ️ يوجد طلب اشتراك مسجل مسبقاً لهذا الحساب، وسيتم التواصل معك قريباً.'];
            }

            $idImageData = null;
            if (isset($context['id_document_image_b64']) && is_string($context['id_document_image_b64'])) {
                $idImageData = base64_decode($context['id_document_image_b64'], true) ?: null;
            }

            $requestId = createSubscriptionRequest(
                $pdo,
                (int) ($botContext['manager_id'] ?? 0),
                (string) ($userIdentity['whatsapp_id'] ?? ''),
                (string) ($context['subscriber_full_name'] ?? ''),
                (string) ($context['subscriber_id_number'] ?? ''),
                (string) ($context['id_document_image_url'] ?? ''),
                '',
                (string) ($context['exact_location'] ?? ''),
                $text,
                $idImageData
            );

            clearConversation($pdo, $botContext['bot_id'], $chatId);
            return [
                'text' => "✅ تم استلام طلب الاشتراك رقم #{$requestId}. سنقوم بالرد عليك في أقرب وقت ممكن.",
                'type' => 'interactive_buttons',
                'buttons' => [
                    ['id' => 'action_new_transfer', 'title' => '💸 تحويل جديد'],
                    ['id' => 'action_support', 'title' => '🛠️ الدعم الفني'],
                ],
            ];

        // ── Support Ticket Flow ────────────────────────────
        case 'support_ticket_message':
            if ($text === '') {
                return ['text' => '⚠️ يرجى كتابة الشكوى باختصار.'];
            }

            $context['complaint_message'] = $text;
            saveConversation($pdo, $botContext['bot_id'], $chatId, 'support_ticket_mobile', $context);
            return ['text' => '📱 يرجى كتابة رقم الجوال للتواصل معك.'];

        case 'support_ticket_mobile':
            $mobile = normalizeMobile($text);
            if ($mobile === null) {
                return ['text' => '⚠️ رقم الجوال غير صحيح. يرجى كتابته بصيغة صحيحة.'];
            }

            $complaintMessage = trim((string) ($context['complaint_message'] ?? ''));
            if ($complaintMessage === '') {
                saveConversation($pdo, $botContext['bot_id'], $chatId, 'support_ticket_message', $context);
                return ['text' => '⚠️ يرجى كتابة الشكوى أولاً.'];
            }

            if (hasOpenSupportTicketByWhatsAppId($pdo, (int) ($botContext['manager_id'] ?? 0), (string) ($userIdentity['whatsapp_id'] ?? ''))) {
                clearConversation($pdo, $botContext['bot_id'], $chatId);
                return ['text' => '⚠️ لديك شكوى مفتوحة حالياً. لا يمكن إضافة شكوى جديدة حتى يتم تحديث الحالة.'];
            }

            $ticketNo = createSupportTicket($pdo, $botContext, $userIdentity, $complaintMessage, $mobile);
            clearConversation($pdo, $botContext['bot_id'], $chatId);
            return [
                'text' => "✅ الشكوى الخاصة بك قيد الفحص. الرجاء انتظار الرد في أسرع وقت ممكن. رقم التذكرة: #{$ticketNo}",
                'type' => 'interactive_buttons',
                'buttons' => [
                    ['id' => 'action_new_transfer', 'title' => '💸 تحويل جديد'],
                    ['id' => 'action_support', 'title' => '🛠️ الدعم الفني'],
                ],
            ];

        default:
            logMessage('[state-step] Unknown state "' . $state . '" for chat_id=' . $chatId . ', clearing conversation');
            clearConversation($pdo, $botContext['bot_id'], $chatId);
            return [
                'text' => '⚠️ تم إنهاء العملية السابقة. ابدأ من جديد.',
                'type' => 'interactive_buttons',
                'buttons' => [
                    ['id' => 'action_new_transfer', 'title' => '💸 تحويل جديد'],
                    ['id' => 'action_support', 'title' => '🛠️ الدعم الفني'],
                ],
            ];
    }
}

// ─────────────────────────────────────────────────────────────
// Webhook Entry Point
// ─────────────────────────────────────────────────────────────

function processWebhookPayload(array $config): void
{
    $rawBody = file_get_contents('php://input');

    logMessage('Webhook raw payload (' . strlen($rawBody ?: '') . ' bytes): ' . mb_substr($rawBody ?: '', 0, 2000));

    if ($rawBody === false || $rawBody === '') {
        logMessage('Webhook ignored: empty body');
        http_response_code(200);
        echo 'OK';
        return;
    }

    $payload = json_decode($rawBody, true);
    if (!is_array($payload)) {
        logMessage('Webhook ignored: invalid JSON');
        http_response_code(200);
        echo 'OK';
        return;
    }

    if (!validateIncomingWebhookSecret($config, $payload)) {
        http_response_code(403);
        echo 'Forbidden';
        return;
    }

    logMessage('Webhook received: ' . mb_substr($rawBody, 0, 1000));

    if (logMessageUpdateEvent($payload)) {
        http_response_code(200);
        echo 'OK';
        return;
    }

    $normalized = normalizeIncomingWebhookPayload($payload);
    if (!is_array($normalized)) {
        $data = (array) ($payload['data'] ?? []);
        $eventType = (string) ($data['event'] ?? $payload['event_type'] ?? $payload['event'] ?? $payload['type'] ?? 'unknown');
        logMessage('Webhook event ignored/unrecognized: event_type=' . $eventType . ' top_keys=' . implode(',', array_keys($payload)));
        http_response_code(200);
        echo 'OK';
        return;
    }

    // Skip our own messages
    if (($normalized['from_me'] ?? false) === true) {
        logMessage('[webhook] Skipping own message (from_me=true)');
        http_response_code(200);
        echo 'OK';
        return;
    }

    // FIXED: Skip group messages — bot only responds to private chats
    $remoteJid = (string) ($normalized['remote_jid'] ?? '');
    if (isGroupJid($remoteJid)) {
        logMessage('[webhook] Skipping group message: jid=' . $remoteJid . ' sender=' . ($normalized['sender_phone'] ?? ''));
        http_response_code(200);
        echo 'OK';
        return;
    }

    if (shouldSkipDuplicateInbound($normalized)) {
        http_response_code(200);
        echo 'OK';
        return;
    }

    $senderPhone = (string) ($normalized['sender_phone'] ?? '');
    if ($senderPhone === '') {
        logMessage('Webhook ignored: sender phone is empty');
        http_response_code(200);
        echo 'OK';
        return;
    }

    $msgType = (string) ($normalized['msg_type'] ?? 'chat');
    $msgBody = (string) ($normalized['body'] ?? '');
    $contactName = (string) ($normalized['contact_name'] ?? '');

    logMessage('Incoming WhatsApp message from ' . $senderPhone . ' event=' . (string) ($normalized['event_type'] ?? '') . ' type=' . $msgType . ' body=' . mb_substr($msgBody, 0, 100));

    $messageData = [
        'from' => $senderPhone,
        'id' => (string) ($normalized['id'] ?? ''),
        'type' => $msgType,
        'body' => '',
    ];

    if ($msgType === 'chat') {
        $messageData['body'] = $msgBody;
    } elseif (in_array($msgType, ['image', 'sticker'], true)) {
        $mediaUrl = (string) ($normalized['image_url'] ?? '');
        $mediaBase64 = (string) ($normalized['image_base64'] ?? '');
        $messageData['_image_url'] = $mediaUrl;
        $messageData['_image_base64'] = $mediaBase64;
        $messageData['body'] = (string) ($normalized['caption'] ?? $msgBody);
    } elseif ($msgType === 'document') {
        $messageData['body'] = (string) ($normalized['caption'] ?? $msgBody);
    } else {
        $messageData['body'] = $msgBody;
    }

    logMessage('[webhook] Connecting to database...');
    $pdo = waDb($config);
    ensureFlowSchema($pdo);
    logMessage('[webhook] DB connected OK');

    $botContext = findBotContext($pdo, $config);
    logMessage('[webhook] Bot context: bot_id=' . ($botContext['bot_id'] ?? '?') . ' manager_id=' . ($botContext['manager_id'] ?? '?') . ' brand=' . ($botContext['brand_name'] ?? ''));

    $userIdentity = buildWhatsAppUserIdentity($senderPhone, $contactName);
    logMessage('[webhook] Processing message from ' . $senderPhone . ' (' . ($userIdentity['display_name'] ?? '') . ')');

    $response = handleBotUpdate($pdo, $botContext, $config, $messageData, $userIdentity);
    logMessage('[webhook] Bot response ready: type=' . ($response['type'] ?? 'text') . ' text=' . mb_substr($response['text'] ?? '', 0, 100));

    sendBotResponse($config, $senderPhone, $response, $pdo, $botContext);

    logMessage('[webhook] Request completed for ' . $senderPhone);
    http_response_code(200);
    echo 'OK';
}

// ─────────────────────────────────────────────────────────────
// Main Execution
// ─────────────────────────────────────────────────────────────

try {
    $config = loadBotConfig();
    validateBotConfig($config);

    $requestMethod = $_SERVER['REQUEST_METHOD'] ?? 'GET';

    if ($requestMethod === 'GET') {
        http_response_code(200);
        echo 'WhatsApp Bot Webhook Active';
    } else {
        processWebhookPayload($config);
    }
} catch (Throwable $e) {
    logMessage('Fatal error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
    logMessage('Stack trace: ' . $e->getTraceAsString());
    http_response_code(500);
    echo 'Internal Server Error';
    exit(1);
}
