<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\AttachmentPolicy;
use EasyChat\Library\AttachmentStorage;
use EasyChat\Library\CustomerContactService;
use EasyChat\Library\Logger;
use EasyChat\Library\SafeRuntime;
use EasyChat\Model\Message;
use EasyChat\Model\Messenger;
use EasyChat\Model\Setting;
use PhpWA\Client;
use PhpWA\Integration\EventType;
use PhpWA\Runtime\Deadline;
use PhpWA\Runtime\OutboxHandlerInterface;
use PhpWA\Storage\OutboxEvent;

 
final class WhatsAppPersonalOutboxHandler implements OutboxHandlerInterface {
    private const HISTORY_WINDOW_DAYS = 30;
    private $registry;
    private $channel;
    private $client;
    private $identities;
    private $avatar_lookups = 0;

    public function __construct($registry, array $channel, Client $client) {
        $this->registry = $registry;
        $this->channel = $channel;
        $this->client = $client;
        $this->identities = new WhatsAppPersonalIdentityRepository($registry);
    }

    public function handle(string $accountId, OutboxEvent $event, Deadline $deadline): void {
        $payload = json_decode($event->payload(), true, 128, JSON_THROW_ON_ERROR);
        if (!is_array($payload) || (int)($payload['schema_version'] ?? 0) < 1) {
            throw new \RuntimeException('whatsapp_event_payload_invalid');
        }
        $type = $event->type();
        if ($type === EventType::HISTORY_MAPPING) {
            $this->identities->rememberMapping(
                (int)$this->channel['channel_id'],
                (string)($payload['pn_jid'] ?? ''),
                (string)($payload['lid_jid'] ?? ''),
                'history_mapping'
            );
            return;
        }
        if ($type === EventType::HISTORY_CHAT) {
            $this->identities->rememberProfile(
                (int)$this->channel['channel_id'],
                (string)($payload['jid'] ?? ''),
                (string)($payload['name'] ?? ''),
                '',
                (string)($payload['pn_jid'] ?? ''),
                (string)($payload['lid_jid'] ?? '')
            );
            return;
        }
        if ($type === EventType::HISTORY_PUSHNAME) {
            $this->identities->rememberProfile(
                (int)$this->channel['channel_id'],
                (string)($payload['jid'] ?? ''),
                (string)($payload['push_name'] ?? '')
            );
            return;
        }
        if ($type === EventType::MESSAGE_RECEIPT) {
            $this->handleReceipt($payload);
            return;
        }
        if (!in_array($type, [EventType::MESSAGE_RECEIVED, EventType::HISTORY_MESSAGE], true)) {
            return;
        }
         
         
        if ((string)($payload['kind'] ?? '') === 'protocol') {
            return;
        }
        if ($type === EventType::HISTORY_MESSAGE && !$this->withinHistoryWindow($payload)) {
             
             
             
            return;
        }

        $this->rememberLiveIdentity($payload, $deadline);
        $download = [];
        $media_download_failed = false;
        try {
            $download = $this->downloadMedia($payload, $event->id(), $deadline);
        } catch (\PhpWA\Exception\ProtocolException | \PhpWA\Exception\TransportException | \PhpWA\Exception\TimeoutException $error) {
             
             
             
             
             
            $media_download_failed = true;
        }
        $public_payload = [
            '_phpwa_event' => $this->boundedEvent($payload),
            '_easychat_channel_id' => (int)$this->channel['channel_id'],
            '_easychat_event_id' => $event->id(),
            '_easychat_trusted_worker' => true,
            '_easychat_history' => $type === EventType::HISTORY_MESSAGE,
            '_easychat_media_pending' => $download !== [],
            '_easychat_media_download_failed' => $media_download_failed,
        ];
        $raw = json_encode($public_payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
        try {
            $result = (new InboundService($this->registry))->handleWebhook(
                WhatsAppPersonalProvider::PROVIDER,
                $this->channel,
                $public_payload,
                (string)$raw,
                '',
                $type === EventType::HISTORY_MESSAGE
            );
            if (empty($result['success'])) {
                throw new \RuntimeException('whatsapp_inbound_projection_failed');
            }
            $this->confirmPhone($payload, (int)($result['chat_id'] ?? 0));
            if ($download !== []) {
                $this->attachDownloadedMedia($download, $payload, $result, $event->id());
            }
        } finally {
             
             
            if (!empty($download['path']) && is_file((string)$download['path'])) {
                SafeRuntime::unlink((string)$download['path']);
            }
        }
    }

    private function rememberLiveIdentity(array $payload, Deadline $deadline): void {
        $channel_id = (int)$this->channel['channel_id'];
        $sender = (string)($payload['sender_jid'] ?? '');
        $alternate = (string)($payload['alternate_sender_jid'] ?? '');
        $phone = $this->phoneJid($sender) ?: $this->phoneJid($alternate);
        $lid = $this->lidJid($sender) ?: $this->lidJid($alternate);
        if ($phone !== '' && $lid !== '') {
            $this->identities->rememberMapping($channel_id, $phone, $lid, 'live_envelope');
        }
        $chat = (string)($payload['chat_jid'] ?? '');
        $name = (string)($payload['push_name'] ?? '');
        $group = $this->isGroup($chat);
        if ($group && $sender !== '') {
            $this->identities->rememberProfile($channel_id, $sender, $name, '', $phone, $lid);
        }
        $avatar = '';
        $known = $this->identities->resolve($channel_id, $chat);
        if ((string)($known['avatar_url'] ?? '') === '' && $this->avatar_lookups < 5 && $chat !== '') {
            $this->avatar_lookups++;
            try {
                $avatar = (string)$this->client->profilePictureUrl($chat, $deadline, true);
            } catch (\Throwable $ignored) {
                $avatar = '';
            }
        }
        $this->identities->rememberProfile(
            $channel_id,
            $chat,
            $group ? '' : $name,
            $avatar,
            $group ? '' : $phone,
            $group ? '' : $lid
        );
    }

    private function handleReceipt(array $payload): void {
        $type = (string)($payload['type'] ?? '');
        if (!in_array($type, ['read', 'played'], true)) {
            if ($type !== '') {
                $this->logReceiptProjection(
                    (int)$this->channel['channel_id'],
                    $type,
                    (string)($payload['from_jid'] ?? ''),
                    is_array($payload['message_ids'] ?? null) ? count($payload['message_ids']) : 0,
                    ['success' => true, 'ignored' => true, 'error' => 'receipt_type_not_customer_read']
                );
            }
            return;
        }
        $channel_id = (int)$this->channel['channel_id'];
        $from = (string)($payload['from_jid'] ?? '');
        $participant = (string)($payload['participant_jid'] ?? '');
        $ids = is_array($payload['message_ids'] ?? null) ? $payload['message_ids'] : [];
        if ($from === '' || $ids === []) {
            $this->logReceiptProjection($channel_id, $type, '', count($ids), ['success' => false, 'error' => 'receipt_fields_missing']);
            return;
        }

        $resolved = $this->identities->resolve($channel_id, $from);
        $aliases = array_values(array_unique(array_filter([
            $from,
            (string)($resolved['lid_jid'] ?? ''),
            (string)($resolved['phone_jid'] ?? ''),
        ])));
        $reader = $participant !== '' ? $participant : $from;
        $service = new MessageReadReceiptService($this->registry);
        foreach ($aliases as $chat_jid) {
             
             
             
             
            $result = $service->observeWhatsApp(
                $channel_id,
                $chat_jid,
                $reader,
                $ids,
                max(0, (int)($payload['timestamp'] ?? 0))
            );
            $this->logReceiptProjection($channel_id, $type, $chat_jid, count($ids), $result);
            if ((int)($result['changed'] ?? 0) > 0 || !empty($result['ignored'])) {
                break;
            }
        }
    }

     
    private function logReceiptProjection(int $channel_id, string $type, string $chat_jid, int $message_count, array $result): void {
        $diagnostic = [
            'event' => 'whatsapp_receipt_projection',
            'channel_id' => $channel_id,
            'type' => $type,
            'chat_hash' => $chat_jid !== '' ? substr(hash('sha256', $chat_jid), 0, 16) : '',
            'message_count' => max(0, $message_count),
            'success' => !empty($result['success']),
            'changed' => max(0, (int)($result['changed'] ?? 0)),
            'ignored' => !empty($result['ignored']),
            'error' => preg_replace('/[^a-z0-9_]/', '', (string)($result['error'] ?? '')),
        ];
        Logger::info(json_encode($diagnostic, JSON_UNESCAPED_SLASHES), 'easychat-whatsapp-receipts.log');
    }

    private function confirmPhone(array $payload, int $chat_id): void {
        if ($chat_id <= 0 || !empty($payload['from_me']) || $this->isGroup((string)($payload['chat_jid'] ?? ''))) {
            return;
        }
        $phone = $this->identities->confirmedPhone(
            (int)$this->channel['channel_id'],
            (string)($payload['sender_jid'] ?? ''),
            (string)($payload['alternate_sender_jid'] ?? '')
        );
        if ($phone !== '') {
            (new CustomerContactService($this->registry))->recordProviderConfirmedPhoneForChat(
                $chat_id,
                $phone,
                'whatsapp_protocol_mapping',
                WhatsAppPersonalProvider::PROVIDER
            );
        }
    }

    private function downloadMedia(array $payload, string $event_id, Deadline $deadline): array {
        $media = is_array($payload['media'] ?? null) ? $payload['media'] : [];
        if ($media === [] || empty($media['downloadable'])) {
            return [];
        }
        $length = (string)($media['file_length'] ?? '0');
        $max_mb = max(1, min(32, (int)(new Setting($this->registry))->get('chat_max_file_size', '5')));
        if (preg_match('/^(0|[1-9][0-9]{0,9})$/D', $length) !== 1 || (int)$length <= 0 || (int)$length > $max_mb * 1024 * 1024) {
            return [];
        }
        $name = $this->mediaName($media);
        if ($name === '') {
            return [];
        }
        $root = WhatsAppPersonalRuntime::privateRoot() . DIRECTORY_SEPARATOR . 'media-staging'
            . DIRECTORY_SEPARATOR . (int)$this->channel['channel_id'];
        if (!is_dir($root) && !SafeRuntime::mkdir($root, 0700, true)) {
            throw new \RuntimeException('whatsapp_media_staging_unavailable');
        }
        if (!SafeRuntime::chmod($root, 0700) || SafeRuntime::permissions($root) !== 0700) {
            throw new \RuntimeException('whatsapp_media_staging_permissions_invalid');
        }
        $path = $root . DIRECTORY_SEPARATOR . substr($event_id, 0, 32) . '-' . bin2hex(random_bytes(6))
            . '.' . strtolower(pathinfo($name, PATHINFO_EXTENSION));
        $this->client->downloadIncomingMedia($media, $path, $deadline);
        return [
            'path' => $path,
            'root' => $root,
            'name' => $name,
            'sha256' => hash_file('sha256', $path),
        ];
    }

    private function attachDownloadedMedia(array $download, array $payload, array $result, string $event_id): void {
        $chat_id = (int)($result['chat_id'] ?? 0);
        $message_id = (int)($result['message_id'] ?? 0);
        try {
            if ($chat_id <= 0 || $message_id <= 0) {
                throw new \RuntimeException('whatsapp_media_projection_missing');
            }
            $storage = new AttachmentStorage($this->registry);
            if (!$storage->acquireMessageAttachmentLock($message_id)) {
                throw new \RuntimeException('whatsapp_media_projection_busy');
            }
            try {
                $known = $storage->getAttachmentsByMessageIds([$message_id]);
                foreach ($known[$message_id] ?? [] as $row) {
                    $meta = json_decode((string)($row['meta_json'] ?? '{}'), true);
                    if (is_array($meta) && hash_equals((string)($meta['phpwa_event_id'] ?? ''), $event_id)) {
                        return;
                    }
                }
                $file = $storage->importTrustedProviderFile(
                    $chat_id,
                    (string)$download['path'],
                    (string)$download['root'],
                    (string)$download['name'],
                    (string)$download['sha256'],
                    32
                );
                if (empty($file['success'])) {
                    throw new \RuntimeException('whatsapp_media_validation_failed');
                }
                $storage->attachToMessage($message_id, $chat_id, $file, WhatsAppPersonalProvider::PROVIDER, [
                    'phpwa_event_id' => $event_id,
                    'external_payload_hash' => hash('sha256', json_encode($payload['media'], JSON_UNESCAPED_SLASHES)),
                ]);
                if ((string)($file['kind'] ?? '') === 'image') {
                    (new Message($this->registry))->setImageIfEmpty($message_id, (string)$file['filename']);
                }
            } finally {
                $storage->releaseMessageAttachmentLock($message_id);
            }
        } finally {
            if (!empty($download['path']) && is_file($download['path'])) {
                SafeRuntime::unlink((string)$download['path']);
            }
        }
    }

     
    private function boundedEvent(array $payload): array {
        unset($payload['message_proto_base64'], $payload['message_proto_sha256']);
        if (isset($payload['media'])) {
             
             
            $payload['media'] = null;
        }
        return $payload;
    }

    private function mediaName(array $media): string {
        $name = basename(trim((string)($media['file_name'] ?? '')));
        $mime = strtolower(trim((string)($media['mimetype'] ?? '')));
        $kind = strtolower((string)($media['type'] ?? ''));
        $extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
        if ($extension === '') {
            $extension = AttachmentPolicy::extensionForMime($mime);
        }
        if ($extension === '' && $kind === 'sticker') $extension = 'webp';
        if (!in_array($extension, AttachmentPolicy::allowedAttachmentExtensions(true), true)
            && !AttachmentPolicy::isAudioExtension($extension)) {
            return '';
        }
        return $name !== '' ? $name : $kind . '.' . $extension;
    }

    private function phoneJid(string $jid): string {
        $jid = strtolower(trim($jid));
        $jid = preg_replace('/:[0-9]+@/', '@', $jid);
        return preg_match('/^[1-9][0-9]{0,19}@s\.whatsapp\.net$/D', $jid) === 1 ? $jid : '';
    }

    private function lidJid(string $jid): string {
        $jid = strtolower(trim($jid));
        $jid = preg_replace('/:[0-9]+@/', '@', $jid);
        return preg_match('/^[1-9][0-9]{0,19}@lid$/D', $jid) === 1 ? $jid : '';
    }

    private function isGroup(string $jid): bool {
        return substr($jid, -5) === '@g.us' || substr($jid, -10) === '@broadcast';
    }

    private function withinHistoryWindow(array $payload): bool {
        $timestamp = $payload['timestamp'] ?? 0;
        if (is_string($timestamp) && preg_match('/^[1-9][0-9]{0,18}$/D', $timestamp) === 1) {
            $timestamp = (int)$timestamp;
        }
        if (!is_int($timestamp) || $timestamp <= 0 || $timestamp > time() + 300) {
            return false;
        }
        return $timestamp >= time() - self::HISTORY_WINDOW_DAYS * 86400;
    }
}
