<?php
namespace EasyChat\Library\Messenger;

use EasyChat\Library\AttachmentPolicy;
use EasyChat\Library\Http\ExternalHttpClient;
use EasyChat\Library\Queue\QueueManager;
use EasyChat\Library\SafeRuntime;
use EasyChat\Model\Messenger as MessengerModel;
use EasyChat\Model\Session;
use EasyChat\Model\Setting;







class ContactAvatarService {
    private const SETTING_KEY = 'chat_messenger_avatars_status';
    private const AVATAR_SIZE = 192;
    private const MAX_DOWNLOAD_BYTES = 1048576;
    private const RETRY_COOLDOWN = 86400;
    private const PROFILE_MISSING_ERROR = 'avatar_url_missing';
    private const PROFILE_LOOKUP_FAILED_ERROR = 'avatar_profile_lookup_failed';
    private const PROFILE_MISSING_RETRY_COOLDOWN = 1800;
    private const PROFILE_MISSING_ACTIVITY_RETRY_COOLDOWN = 300;
    private const PROFILE_LOOKUP_FAILED_RETRY_COOLDOWN = 300;
    private const SCHEDULED_BACKFILL_LAST_RUN_KEY = 'chat_messenger_avatar_backfill_last_run_at';
    private const SCHEDULED_BACKFILL_LOCK_KEY = 'chat_messenger_avatar_backfill_lock_until';
    private const SCHEDULED_BACKFILL_DEFAULT_INTERVAL = 900;
    private const SCHEDULED_BACKFILL_DEFAULT_LIMIT = 50;
    private const SCHEDULED_BACKFILL_LOCK_TTL = 120;

    private $registry;
    private $db;
    private $prefix;
    private $http;

    public function __construct($registry, ?ExternalHttpClient $http = null) {
        $this->registry = $registry;
        $this->db = $registry->get('db');
        $this->prefix = easychat_db_prefix();
        $this->http = $http ?: new ExternalHttpClient($registry);
    }

    public function isEnabled(): bool {
        return (new Setting($this->registry))->get(self::SETTING_KEY, '0') === '1';
    }

    public function enqueueFromEvent(array $channel, array $contact, array $event): bool {
        if (!$this->isEnabled()) {
            return false;
        }

        $contact_id = (int)($contact['contact_id'] ?? 0);
        $provider = strtolower((string)($contact['provider'] ?? $event['provider'] ?? $channel['provider'] ?? ''));
        if ($contact_id <= 0 || $provider === '') {
            return false;
        }

        $identity_fingerprint = $this->identityFingerprintForContact($provider, $contact);
        if ($provider === 'max' && $identity_fingerprint === '') {
            return false;
        }
        $contact = $this->resetStaleMaxAvatar($provider, $contact, $identity_fingerprint);
        $avatar_url = trim((string)($event['avatar_url'] ?? ''));
        if ($avatar_url !== '' && !$this->isAllowedAvatarUrl($provider, $avatar_url)) {
            $this->markChecked($contact_id, $this->sourceHash($provider, $avatar_url), 'avatar_url_denied', $identity_fingerprint);
            $avatar_url = '';
        }

        if ($avatar_url !== '') {
            $source_hash = $this->sourceHash($provider, $avatar_url);
            if (!$this->shouldQueue($contact, $source_hash)) {
                return false;
            }
            return $this->pushJob([
                'mode' => 'url',
                'provider' => $provider,
                'channel_id' => (int)($channel['channel_id'] ?? 0),
                'contact_id' => $contact_id,
                'avatar_url' => $avatar_url,
                'source_hash' => $source_hash,
                'identity_fingerprint' => $identity_fingerprint,
            ]);
        }

        if (in_array($provider, ['avito', 'youla', 'vk', 'telegram', 'max'], true)) {
            $profile_identity = $this->providerProfileIdentity($provider, $contact);
            if ($profile_identity === '') {
                return false;
            }
            $source_hash = $this->sourceHash($provider, 'provider_profile:' . $profile_identity);
            if (!$this->shouldQueueProviderProfile($contact, $source_hash)) {
                return false;
            }
            return $this->pushJob([
                'mode' => 'provider_profile',
                'provider' => $provider,
                'channel_id' => (int)($channel['channel_id'] ?? 0),
                'contact_id' => $contact_id,
                'source_hash' => $source_hash,
                'identity_fingerprint' => $identity_fingerprint,
            ]);
        }

        return false;
    }

    public function enqueueBackfill(int $limit = 300): int {
        if (!$this->isEnabled()) {
            return 0;
        }

        $limit = max(1, min(1000, $limit));
        $query = $this->db->query(
            "SELECT *
             FROM `{$this->prefix}messenger_contacts`
             WHERE `provider` IN ('avito','youla','vk','telegram','max')
               AND `chat_id` IS NOT NULL
               AND (
                    `avatar_file` = ''
                    OR `avatar_file` IS NULL
                    OR (
                        `provider` = 'max'
                        AND LOCATE(
                            CONCAT('\"identity_fingerprint\":\"', `avatar_identity_fingerprint`, '\"'),
                            `metadata_json`
                        ) = 0
                    )
               )
             ORDER BY `last_seen_at` DESC, `contact_id` DESC
             LIMIT " . $limit
        );

        $queued = 0;
        foreach ($query->rows as $contact) {
            $provider = strtolower((string)$contact['provider']);
            $identity_fingerprint = $this->identityFingerprintForContact($provider, $contact);
            $contact = $this->resetStaleMaxAvatar($provider, $contact, $identity_fingerprint);
            $profile_identity = $this->providerProfileIdentity($provider, $contact);
            if ($profile_identity === '') {
                continue;
            }
            $source_hash = $this->sourceHash($provider, 'provider_profile:' . $profile_identity);
            if (!$this->shouldQueueProviderProfile($contact, $source_hash)) {
                continue;
            }
            if ($this->pushJob([
                'mode' => 'provider_profile',
                'provider' => $provider,
                'channel_id' => (int)$contact['channel_id'],
                'contact_id' => (int)$contact['contact_id'],
                'source_hash' => $source_hash,
                'identity_fingerprint' => $identity_fingerprint,
            ])) {
                $queued++;
            }
        }

        return $queued;
    }

    public function enqueueScheduledBackfill(int $interval = self::SCHEDULED_BACKFILL_DEFAULT_INTERVAL, int $limit = self::SCHEDULED_BACKFILL_DEFAULT_LIMIT): array {
        $interval = max(300, min(86400, (int)$interval));
        $limit = max(1, min(200, (int)$limit));
        $result = [
            'ran' => false,
            'queued' => 0,
            'skipped' => false,
            'reason' => '',
            'interval' => $interval,
            'limit' => $limit,
        ];

        if (!$this->isEnabled()) {
            $result['skipped'] = true;
            $result['reason'] = 'disabled';
            return $result;
        }

        $now = time();
        $slot = $this->acquireScheduledBackfillSlot($now, $interval);
        if (empty($slot['acquired'])) {
            $result['skipped'] = true;
            $result['reason'] = (string)($slot['reason'] ?? 'locked');
            return $result;
        }

        try {
            $result['queued'] = $this->enqueueBackfill($limit);
            $result['ran'] = true;
            (new Setting($this->registry))->set(self::SCHEDULED_BACKFILL_LAST_RUN_KEY, (string)$now);
        } finally {
            $this->releaseScheduledBackfillSlot();
        }

        return $result;
    }

    public function handleQueuePayload(array $payload): void {
        if (!$this->isEnabled()) {
            return;
        }

        $contact_id = (int)($payload['contact_id'] ?? 0);
        if ($contact_id <= 0) {
            throw new \Exception('contact_avatar_payload_invalid');
        }

        $model_messenger = new MessengerModel($this->registry);
        $contact = $model_messenger->getContactById($contact_id);
        if (!$contact) {
            throw new \Exception('contact_avatar_contact_missing');
        }

        $provider = strtolower((string)($payload['provider'] ?? $contact['provider'] ?? ''));
        if (!$this->isPayloadIdentityCurrent($provider, $payload, $contact)) {
            return;
        }
        $channel = $model_messenger->getChannelById((int)($contact['channel_id'] ?? $payload['channel_id'] ?? 0));
        if (!$channel) {
            throw new \Exception('contact_avatar_channel_missing');
        }

        $avatar_url = '';
        $profile_lookup = null;
        $mode = (string)($payload['mode'] ?? 'url');
        if ($mode === 'url') {
            $avatar_url = trim((string)($payload['avatar_url'] ?? ''));
        } else {
            $profile_lookup = $this->resolveProviderProfileAvatarResult($provider, $channel, $contact);
            $contact = $model_messenger->getContactById($contact_id) ?: $contact;
            if (!$this->isPayloadIdentityCurrent($provider, $payload, $contact)) {
                return;
            }
            if (empty($profile_lookup['success'])) {
                $this->markChecked(
                    $contact_id,
                    (string)($payload['source_hash'] ?? ''),
                    self::PROFILE_LOOKUP_FAILED_ERROR,
                    (string)($payload['identity_fingerprint'] ?? '')
                );
                return;
            }
            $avatar_url = trim((string)($profile_lookup['avatar_url'] ?? ''));
        }

        if ($avatar_url === '') {
            $this->markChecked(
                $contact_id,
                (string)($payload['source_hash'] ?? ''),
                self::PROFILE_MISSING_ERROR,
                (string)($payload['identity_fingerprint'] ?? '')
            );
            return;
        }
        if (!$this->isAllowedAvatarUrl($provider, $avatar_url)) {
            $this->markChecked(
                $contact_id,
                $this->sourceHash($provider, $avatar_url),
                'avatar_url_denied',
                (string)($payload['identity_fingerprint'] ?? '')
            );
            return;
        }

        $source_hash = $this->sourceHash($provider, $avatar_url);
        $fresh_contact = $model_messenger->getContactById($contact_id) ?: $contact;
        if (!$this->isPayloadIdentityCurrent($provider, $payload, $fresh_contact)) {
            return;
        }
        if (!$this->shouldProcess($fresh_contact, $source_hash)) {
            return;
        }

        $prepared = $this->downloadAndPrepare($provider, $avatar_url, $contact_id, $source_hash);
        if (empty($prepared['success'])) {
            $this->markChecked(
                $contact_id,
                $source_hash,
                (string)($prepared['error'] ?? 'avatar_prepare_failed'),
                (string)($payload['identity_fingerprint'] ?? '')
            );
            return;
        }

        $latest_contact = $model_messenger->getContactById($contact_id) ?: $fresh_contact;
        if (!$this->isPayloadIdentityCurrent($provider, $payload, $latest_contact)) {
            SafeRuntime::unlink($this->avatarDir() . basename((string)($prepared['file'] ?? '')));
            return;
        }
        $old_file = (string)($latest_contact['avatar_file'] ?? '');
        $updated = $model_messenger->updateContactAvatar($contact_id, [
            'avatar_file' => $prepared['file'],
            'avatar_mime' => $prepared['mime'],
            'avatar_source_hash' => $source_hash,
            'avatar_identity_fingerprint' => (string)($payload['identity_fingerprint'] ?? ''),
            'expected_identity_fingerprint' => (string)($payload['identity_fingerprint'] ?? ''),
            'avatar_last_error' => '',
        ]);
        if (!$updated) {
            SafeRuntime::unlink($this->avatarDir() . basename((string)$prepared['file']));
            return;
        }

        if (!empty($latest_contact['chat_id'])) {
            (new Session($this->registry))->updateModifiedDate((int)$latest_contact['chat_id']);
        }
        if ($old_file !== '' && $old_file !== $prepared['file']) {
            SafeRuntime::unlink($this->avatarDir() . basename($old_file));
        }
    }

    public function avatarUrlFromFields(array $row, string $route): string {
        if (!$this->isEnabled()) {
            return '';
        }
        $file = basename(trim((string)($row['avatar_file'] ?? '')));
        if ($file === '') {
            return '';
        }
        $version = substr((string)($row['avatar_source_hash'] ?? ''), 0, 16);
        return HTTP_SERVER . 'index.php?route=' . rawurlencode($route) . '&file=' . rawurlencode($file) . ($version !== '' ? '&v=' . rawurlencode($version) : '');
    }

    public function avatarVersionFromFields(array $row): string {
        return substr((string)($row['avatar_source_hash'] ?? ''), 0, 16);
    }

    public function touchChatsWithPreparedAvatars(): int {
        $this->db->query(
            "UPDATE `{$this->prefix}sessions`
             SET `date_modified` = NOW()
             WHERE `external_contact_id` IN (
                 SELECT `contact_id`
                 FROM `{$this->prefix}messenger_contacts`
                 WHERE `avatar_file` IS NOT NULL AND `avatar_file` != ''
             )"
        );
        return (int)$this->db->countAffected();
    }

    public function resolveServableFile(string $file): array {
        if (!$this->isEnabled()) {
            return [];
        }

        $file = basename(trim($file));
        if ($file === '' || !preg_match('/^ca_[a-z0-9_\\.-]+\\.(webp|jpg)$/i', $file)) {
            return [];
        }

        $model_messenger = new MessengerModel($this->registry);
        $contact = $model_messenger->getContactByAvatarFile($file);
        if (!$contact) {
            return [];
        }

        $path = $this->avatarDir() . $file;
        if (!is_file($path)) {
            return [];
        }

        $mime = AttachmentPolicy::detectMime($path, pathinfo($path, PATHINFO_EXTENSION));
        if (!in_array($mime, ['image/webp', 'image/jpeg'], true)) {
            return [];
        }

        return [
            'path' => $path,
            'mime' => $mime,
            'etag' => '"' . sha1($file . ':' . (string)filemtime($path) . ':' . (string)filesize($path)) . '"',
        ];
    }

    private function pushJob(array $payload): bool {
        try {
            if ($this->hasActiveAvatarJob($payload)) {
                return false;
            }
            (new QueueManager($this->registry))->push('messenger_avatar_fetch', $payload, 0, 4);
            return true;
        } catch (\Throwable $e) {
            return false;
        }
    }

    private function hasActiveAvatarJob(array $payload): bool {
        $contact_id = (int)($payload['contact_id'] ?? 0);
        if ($contact_id <= 0) {
            return false;
        }

        $contact_needle = '%"contact_id":' . $contact_id . '%';
        $contact_needle_e = $this->db->escape($contact_needle);
        $query = $this->db->query(
            "SELECT `payload`
             FROM `{$this->prefix}queue`
             WHERE `job_type` = 'messenger_avatar_fetch'
               AND `status` IN ('pending','processing')
               AND `payload` LIKE '{$contact_needle_e}'
             ORDER BY `id` DESC
             LIMIT 20"
        );

        foreach ($query->rows as $row) {
            $existing = json_decode((string)($row['payload'] ?? ''), true);
            if (!is_array($existing)) {
                continue;
            }
            if ((int)($existing['contact_id'] ?? 0) !== $contact_id) {
                continue;
            }
            if ((string)($existing['provider'] ?? '') !== (string)($payload['provider'] ?? '')) {
                continue;
            }
            if ((string)($existing['mode'] ?? '') !== (string)($payload['mode'] ?? '')) {
                continue;
            }
            if ((string)($existing['source_hash'] ?? '') !== (string)($payload['source_hash'] ?? '')) {
                continue;
            }
            if ((string)($existing['identity_fingerprint'] ?? '') !== (string)($payload['identity_fingerprint'] ?? '')) {
                continue;
            }
            return true;
        }

        return false;
    }

    private function acquireScheduledBackfillSlot(int $now, int $interval): array {
        $model_setting = new Setting($this->registry);
        $last_run = (int)$model_setting->get(self::SCHEDULED_BACKFILL_LAST_RUN_KEY, '0');
        if ($last_run > 0 && ($now - $last_run) < $interval) {
            return ['acquired' => false, 'reason' => 'recent'];
        }

        $key_e = $this->db->escape(self::SCHEDULED_BACKFILL_LOCK_KEY);
        $lock_until = $now + self::SCHEDULED_BACKFILL_LOCK_TTL;
        $this->db->query(
            "INSERT IGNORE INTO `{$this->prefix}settings` (`key`, `value`)
             VALUES ('{$key_e}', '0')"
        );
        $this->db->query(
            "UPDATE `{$this->prefix}settings`
             SET `value` = '" . (string)$lock_until . "'
             WHERE `key` = '{$key_e}'
               AND CAST(`value` AS UNSIGNED) <= " . $now
        );
        if ((int)$this->db->countAffected() === 0) {
            return ['acquired' => false, 'reason' => 'locked'];
        }

        $last_run = (int)$model_setting->get(self::SCHEDULED_BACKFILL_LAST_RUN_KEY, '0');
        if ($last_run > 0 && ($now - $last_run) < $interval) {
            $this->releaseScheduledBackfillSlot();
            return ['acquired' => false, 'reason' => 'recent'];
        }

        return ['acquired' => true, 'reason' => ''];
    }

    private function releaseScheduledBackfillSlot(): void {
        (new Setting($this->registry))->set(self::SCHEDULED_BACKFILL_LOCK_KEY, '0');
    }

    private function shouldQueue(array $contact, string $source_hash): bool {
        if ($source_hash === '') {
            return false;
        }
        $current_hash = (string)($contact['avatar_source_hash'] ?? '');
        $has_file = trim((string)($contact['avatar_file'] ?? '')) !== '';
        if ($has_file && hash_equals($current_hash, $source_hash)) {
            return false;
        }
        $checked_at = strtotime((string)($contact['avatar_checked_at'] ?? ''));
        if ($checked_at && hash_equals($current_hash, $source_hash) && (time() - $checked_at) < self::RETRY_COOLDOWN) {
            return false;
        }
        return true;
    }

    private function shouldQueueProviderProfile(array $contact, string $source_hash): bool {
        if ($source_hash === '') {
            return false;
        }
        $has_file = trim((string)($contact['avatar_file'] ?? '')) !== '';
        $checked_at = strtotime((string)($contact['avatar_checked_at'] ?? ''));
         
         
         
        if ($has_file && $checked_at && (time() - $checked_at) < self::RETRY_COOLDOWN) {
            return false;
        }
        if (!$has_file && $this->shouldRetryProviderProfileMiss($contact, $source_hash)) {
            return true;
        }
        return $this->shouldQueue($contact, $source_hash);
    }

    private function shouldProcess(array $contact, string $source_hash): bool {
        return $this->shouldQueue($contact, $source_hash);
    }

    private function shouldRetryProviderProfileMiss(array $contact, string $source_hash): bool {
        $current_hash = (string)($contact['avatar_source_hash'] ?? '');
        if ($current_hash === '' || !hash_equals($current_hash, $source_hash)) {
            return false;
        }

        $last_error = (string)($contact['avatar_last_error'] ?? '');
        if (!in_array($last_error, [self::PROFILE_MISSING_ERROR, self::PROFILE_LOOKUP_FAILED_ERROR], true)) {
            return false;
        }

        $cooldown = $last_error === self::PROFILE_LOOKUP_FAILED_ERROR
            ? self::PROFILE_LOOKUP_FAILED_RETRY_COOLDOWN
            : self::PROFILE_MISSING_RETRY_COOLDOWN;

        $checked_at = strtotime((string)($contact['avatar_checked_at'] ?? ''));
        if (!$checked_at) {
            return true;
        }

        $age = time() - $checked_at;
        if ($age >= $cooldown) {
            return true;
        }

        $last_seen_at = strtotime((string)($contact['last_seen_at'] ?? ''));
        return $last_seen_at
            && $last_seen_at > $checked_at
            && $age >= min($cooldown, self::PROFILE_MISSING_ACTIVITY_RETRY_COOLDOWN);
    }

    private function providerProfileIdentity(string $provider, array $contact): string {
        if ($provider === 'youla') {
            $metadata = $this->contactMetadata($contact);
            $buyer_user_id = $this->normalizeYoulaId((string)($metadata['buyer_user_id'] ?? ''));
            if ($buyer_user_id !== '') {
                return $buyer_user_id;
            }
        }
        if ($provider === 'max') {
            $identity = MaxConversationIdentity::fromContact($contact);
            return !empty($identity['resolved']) ? MaxConversationIdentity::profileKey($identity) : '';
        }
        return (string)($contact['external_user_id'] ?? '');
    }

    private function identityFingerprintForContact(string $provider, array $contact): string {
        if ($provider !== 'max') {
            return '';
        }
        $identity = MaxConversationIdentity::fromContact($contact);
        return !empty($identity['resolved']) ? (string)$identity['fingerprint'] : '';
    }

    private function isPayloadIdentityCurrent(string $provider, array $payload, array $contact): bool {
        if ($provider !== 'max') {
            return true;
        }

        $expected = trim((string)($payload['identity_fingerprint'] ?? ''));
        $actual = $this->identityFingerprintForContact($provider, $contact);
        return strlen($expected) === 64
            && strlen($actual) === 64
            && hash_equals($expected, $actual);
    }

    private function resetStaleMaxAvatar(string $provider, array $contact, string $identity_fingerprint): array {
        if ($provider !== 'max' || $identity_fingerprint === '') {
            return $contact;
        }

        $old_file = basename(trim((string)($contact['avatar_file'] ?? '')));
        $stored_fingerprint = trim((string)($contact['avatar_identity_fingerprint'] ?? ''));
        if ($old_file === '' || (strlen($stored_fingerprint) === 64 && hash_equals($stored_fingerprint, $identity_fingerprint))) {
            return $contact;
        }

        $model_messenger = new MessengerModel($this->registry);
        if (!$model_messenger->resetContactAvatarForIdentity(
            (int)($contact['contact_id'] ?? 0),
            $old_file,
            $stored_fingerprint
        )) {
            return $model_messenger->getContactById((int)($contact['contact_id'] ?? 0)) ?: $contact;
        }

        SafeRuntime::unlink($this->avatarDir() . $old_file);
        $contact['avatar_file'] = '';
        $contact['avatar_mime'] = '';
        $contact['avatar_source_hash'] = '';
        $contact['avatar_identity_fingerprint'] = '';
        $contact['avatar_checked_at'] = null;
        $contact['avatar_last_error'] = '';
        return $contact;
    }

    private function resolveProviderProfileAvatar(string $provider, array $channel, array $contact): string {
        $result = $this->resolveProviderProfileAvatarResult($provider, $channel, $contact);
        return !empty($result['success']) ? (string)($result['avatar_url'] ?? '') : '';
    }

    private function resolveProviderProfileAvatarResult(string $provider, array $channel, array $contact): array {
        $credentials = json_decode((string)($channel['credentials_json'] ?? ''), true);
        if (!is_array($credentials)) {
            $credentials = [];
        }

        if ($provider === 'telegram') {
            $token = $this->decryptSecret((string)($credentials['bot_token'] ?? ''));
            $user_id = preg_replace('/\D+/', '', (string)($contact['external_user_id'] ?? ''));
            if ($token === '' || $user_id === '') {
                return $this->profileAvatarMissing();
            }
            $lookup = (new TelegramProvider($this->registry))->getUserProfileAvatarLookup($token, $user_id);
            return !empty($lookup['success'])
                ? $this->profileAvatarUrl((string)($lookup['avatar_url'] ?? ''))
                : $this->profileAvatarLookupFailed();
        }

        if ($provider === 'vk') {
            $token = $this->decryptSecret((string)($credentials['bot_token'] ?? ''));
            $user_id = preg_replace('/\D+/', '', (string)($contact['external_user_id'] ?? ''));
            if ($token === '' || $user_id === '') {
                return $this->profileAvatarMissing();
            }
            $profile = (new VkProvider($this->registry))->getUserInfo($token, $user_id);
            return !empty($profile['success'])
                ? $this->profileAvatarUrl((string)($profile['avatar_url'] ?? ''))
                : $this->profileAvatarLookupFailed();
        }

        if ($provider === 'avito') {
            $token = $this->avitoTokenFromCredentials($credentials);
            $account_user_id = preg_replace('/\D+/', '', (string)($channel['bot_user_id'] ?? ''));
            $external_chat_id = (string)($contact['external_chat_id'] ?? '');
            $metadata = $this->contactMetadata($contact);
            $external_user_id = preg_replace('/\D+/', '', (string)($metadata['counterparty_author_id'] ?? $metadata['author_id'] ?? $contact['external_user_id'] ?? ''));
            if ($external_user_id !== '' && $external_user_id === $account_user_id) {
                 
                 
                 
                $external_user_id = '';
            }
            if ($token === '' || $account_user_id === '' || $external_chat_id === '') {
                return $this->profileAvatarMissing();
            }
            $profile = (new AvitoProvider($this->registry))->getChatUserInfo($token, $account_user_id, $external_chat_id, $external_user_id);
            return !empty($profile['success'])
                ? $this->profileAvatarUrl((string)($profile['avatar_url'] ?? ''))
                : $this->profileAvatarLookupFailed();
        }

        if ($provider === 'youla') {
            $token = $this->youlaTokenFromCredentials($credentials);
            $metadata = $this->contactMetadata($contact);
             
             
             
            $user_id = $this->normalizeYoulaId((string)($metadata['buyer_user_id'] ?? $contact['external_user_id'] ?? ''));
            if ($token === '' || $user_id === '') {
                return $this->profileAvatarMissing();
            }
            $profile = (new YoulaProvider($this->registry))->getUserInfo($token, $user_id, [
                'connect_timeout' => 3,
                'timeout' => 8,
            ]);
            return !empty($profile['success'])
                ? $this->profileAvatarUrl((string)($profile['avatar_url'] ?? ''))
                : $this->profileAvatarLookupFailed();
        }

        return $this->profileAvatarMissing();
    }

    private function extraString($model, array $keys): string {
        if (!is_object($model) || !method_exists($model, 'extra')) {
            return '';
        }
        $extra = $model->extra();
        if (!is_array($extra)) {
            return '';
        }
        foreach ($keys as $key) {
            $value = $extra[$key] ?? null;
            if (is_scalar($value)) {
                $value = trim((string)$value);
                if ($value !== '') {
                    return $value;
                }
            }
        }
        return '';
    }

    private function firstNonEmptyString(array $values): string {
        foreach ($values as $value) {
            if (is_scalar($value)) {
                $value = trim((string)$value);
                if ($value !== '') {
                    return $value;
                }
            }
        }
        return '';
    }

    private function profileAvatarUrl(string $avatar_url): array {
        $avatar_url = trim($avatar_url);
        return [
            'success' => true,
            'avatar_url' => $avatar_url,
            'missing' => $avatar_url === '',
        ];
    }

    private function profileAvatarMissing(): array {
        return $this->profileAvatarUrl('');
    }

    private function profileAvatarLookupFailed(): array {
        return [
            'success' => false,
            'avatar_url' => '',
            'missing' => false,
            'error' => self::PROFILE_LOOKUP_FAILED_ERROR,
        ];
    }

    private function contactMetadata(array $contact): array {
        $metadata = json_decode((string)($contact['metadata_json'] ?? ''), true);
        return is_array($metadata) ? $metadata : [];
    }

    private function downloadAndPrepare(string $provider, string $url, int $contact_id, string $source_hash): array {
        $tmp_dir = $this->avatarDir() . 'tmp' . DS;
        if (!SafeRuntime::mkdir($tmp_dir, 0755, true)) {
            return ['success' => false, 'error' => 'avatar_tmp_dir_failed'];
        }

        $tmp = @tempnam($tmp_dir, 'avatar_');
        if (!is_string($tmp) || $tmp === '') {
            return ['success' => false, 'error' => 'avatar_tmp_file_failed'];
        }

        $fh = fopen($tmp, 'wb');
        if (!$fh) {
            SafeRuntime::unlink($tmp);
            return ['success' => false, 'error' => 'avatar_tmp_open_failed'];
        }

        $bytes = 0;
        $response = $this->http->request('GET', $url, [
            'connect_timeout' => 4,
            'timeout' => 10,
            'follow_location' => true,
            'max_redirects' => 2,
            'trust_profile' => $provider,
            'proxy_profile' => 'messenger_attachments',
            'write_function' => function ($ch, $chunk) use ($fh, &$bytes) {
                $len = strlen($chunk);
                $bytes += $len;
                if ($bytes > self::MAX_DOWNLOAD_BYTES) {
                    return 0;
                }
                fwrite($fh, $chunk);
                return $len;
            },
        ]);
        fclose($fh);

        $http_code = (int)($response['http_code'] ?? 0);
        if (empty($response['transport_ok']) || $http_code < 200 || $http_code >= 300) {
            SafeRuntime::unlink($tmp);
            return ['success' => false, 'error' => 'avatar_download_failed'];
        }
        if ($bytes <= 0 || $bytes > self::MAX_DOWNLOAD_BYTES) {
            SafeRuntime::unlink($tmp);
            return ['success' => false, 'error' => 'avatar_size_denied'];
        }

        $extension = $this->extensionForDownloadedImage($url, (string)($response['content_type'] ?? ''));
        $validation = AttachmentPolicy::validateImageFile($tmp, 'avatar.' . $extension, $bytes, 1, true);
        if (empty($validation['success'])) {
            SafeRuntime::unlink($tmp);
            return ['success' => false, 'error' => (string)($validation['error'] ?? 'avatar_invalid')];
        }

        $prepared = $this->prepareSquareAvatar($tmp, $contact_id, $source_hash);
        SafeRuntime::unlink($tmp);
        return $prepared;
    }

    private function prepareSquareAvatar(string $source, int $contact_id, string $source_hash): array {
        $info = SafeRuntime::imageInfo($source);
        if (!$info || empty($info['mime'])) {
            return ['success' => false, 'error' => 'avatar_image_info_failed'];
        }

        $mime = strtolower((string)$info['mime']);
        $image = $this->createImageFromFile($source, $mime);
        if (!$image) {
            return ['success' => false, 'error' => 'avatar_decode_failed'];
        }

        $src_w = imagesx($image);
        $src_h = imagesy($image);
        if ($src_w <= 0 || $src_h <= 0) {
            $this->destroyImage($image);
            return ['success' => false, 'error' => 'avatar_dimensions_invalid'];
        }

        $side = min($src_w, $src_h);
        $src_x = (int)(($src_w - $side) / 2);
        $src_y = (int)(($src_h - $side) / 2);
        $canvas = imagecreatetruecolor(self::AVATAR_SIZE, self::AVATAR_SIZE);
        if (!$canvas) {
            $this->destroyImage($image);
            return ['success' => false, 'error' => 'avatar_canvas_failed'];
        }
        imagealphablending($canvas, false);
        imagesavealpha($canvas, true);
        $transparent = imagecolorallocatealpha($canvas, 255, 255, 255, 127);
        imagefilledrectangle($canvas, 0, 0, self::AVATAR_SIZE, self::AVATAR_SIZE, $transparent);
        imagecopyresampled($canvas, $image, 0, 0, $src_x, $src_y, self::AVATAR_SIZE, self::AVATAR_SIZE, $side, $side);
        $this->destroyImage($image);

        $use_webp = function_exists('imagewebp');
        $extension = $use_webp ? 'webp' : 'jpg';
        $mime_out = $use_webp ? 'image/webp' : 'image/jpeg';
        $file = 'ca_' . (int)$contact_id . '_' . substr($source_hash, 0, 12) . '_' . time() . '.' . $extension;
        $tmp_out = $this->avatarDir() . $file . '.tmp';
        $path = $this->avatarDir() . $file;

        if ($use_webp) {
            $ok = imagewebp($canvas, $tmp_out, 78);
        } else {
            $white = imagecreatetruecolor(self::AVATAR_SIZE, self::AVATAR_SIZE);
            if (!$white) {
                $this->destroyImage($canvas);
                return ['success' => false, 'error' => 'avatar_canvas_failed'];
            }
            $bg = imagecolorallocate($white, 255, 255, 255);
            imagefilledrectangle($white, 0, 0, self::AVATAR_SIZE, self::AVATAR_SIZE, $bg);
            imagecopy($white, $canvas, 0, 0, 0, 0, self::AVATAR_SIZE, self::AVATAR_SIZE);
            $ok = imagejpeg($white, $tmp_out, 82);
            $this->destroyImage($white);
        }
        $this->destroyImage($canvas);

        if (!$ok || !SafeRuntime::rename($tmp_out, $path)) {
            SafeRuntime::unlink($tmp_out);
            return ['success' => false, 'error' => 'avatar_encode_failed'];
        }

        return [
            'success' => true,
            'file' => $file,
            'mime' => $mime_out,
        ];
    }

    private function destroyImage($image): void {
         
         
        if (function_exists('imagedestroy') && defined('PHP_VERSION_ID') && PHP_VERSION_ID < 80500) {
            @imagedestroy($image);
        }
    }

    private function createImageFromFile(string $path, string $mime) {
        switch ($mime) {
            case 'image/jpeg':
                return imagecreatefromjpeg($path);
            case 'image/png':
                return imagecreatefrompng($path);
            case 'image/gif':
                return imagecreatefromgif($path);
            case 'image/webp':
                return function_exists('imagecreatefromwebp') ? imagecreatefromwebp($path) : false;
            case 'image/bmp':
            case 'image/x-ms-bmp':
                return function_exists('imagecreatefrombmp') ? imagecreatefrombmp($path) : false;
        }
        return false;
    }

    private function markChecked(int $contact_id, string $source_hash, string $error, string $identity_fingerprint = ''): void {
        (new MessengerModel($this->registry))->markContactAvatarChecked(
            $contact_id,
            $source_hash,
            $error,
            $identity_fingerprint
        );
    }

    private function avatarDir(): string {
        $dir = rtrim(DIR_IMAGE, DS) . DS . 'contact_avatars' . DS;
        SafeRuntime::mkdir($dir, 0755, true);
        return $dir;
    }

    private function sourceHash(string $provider, string $source): string {
        $source = trim($source);
        return $source !== '' ? hash('sha256', strtolower($provider) . '|' . $source) : '';
    }

    private function extensionForDownloadedImage(string $url, string $content_type): string {
        $path_ext = strtolower(pathinfo((string)(parse_url($url, PHP_URL_PATH) ?: ''), PATHINFO_EXTENSION));
        if (in_array($path_ext, AttachmentPolicy::allowedImageExtensions(true), true)) {
            return $path_ext;
        }
        $mime = strtolower(trim(explode(';', $content_type)[0]));
        return AttachmentPolicy::extensionForMime($mime) ?: 'jpg';
    }

    private function isAllowedAvatarUrl(string $provider, string $url): bool {
        $parts = parse_url($url);
        if (!is_array($parts) || strtolower((string)($parts['scheme'] ?? '')) !== 'https') {
            return false;
        }
        $host = strtolower((string)($parts['host'] ?? ''));
        if ($host === '') {
            return false;
        }
        $allow = [
            'avito' => ['avito.st', 'avito.ru'],
            'youla' => ['youla.ru', 'youla.io', 'youla.link', 'avatars.mds.yandex.net'],
            'vk' => ['userapi.com', 'vkuserphoto.ru', 'vk-cdn.net', 'vk.com', 'vk.ru'],
            'telegram' => ['api.telegram.org'],
             
             
             
            'max' => ['max.ru', 'i.oneme.ru'],
            'whatsapp-personal' => ['whatsapp.net', 'whatsapp.com'],
        ];
        foreach ($allow[$provider] ?? [] as $domain) {
            if ($host === $domain || substr($host, -strlen('.' . $domain)) === '.' . $domain) {
                return true;
            }
        }
        return false;
    }

    private function decryptSecret(string $encrypted): string {
        $install_secret = (new Setting($this->registry))->get('chat_install_secret', '');
        return $install_secret !== '' ? SecretBox::decrypt($encrypted, $install_secret) : '';
    }

    private function decryptSecretOrPlain(string $stored): string {
        $stored = trim($stored);
        if ($stored === '') {
            return '';
        }
        if (strpos($stored, 'enc:v1:') !== 0) {
            return $stored;
        }
        return $this->decryptSecret($stored);
    }

    private function avitoTokenFromCredentials(array $credentials): string {
        $token = $this->decryptSecretOrPlain((string)($credentials['bot_token'] ?? ''));
        if ($token !== '') {
            return $token;
        }
        $client_id = $this->decryptSecretOrPlain((string)($credentials['client_id'] ?? ''));
        $client_secret = $this->decryptSecretOrPlain((string)($credentials['client_secret'] ?? ''));
        if ($client_id === '' || $client_secret === '') {
            return '';
        }
        return json_encode([
            'client_id' => $client_id,
            'client_secret' => $client_secret,
        ], JSON_UNESCAPED_UNICODE);
    }

    private function youlaTokenFromCredentials(array $credentials): string {
        $token = $this->decryptSecretOrPlain((string)($credentials['bot_token'] ?? ''));
        if ($token !== '') {
            return $token;
        }
        return $this->decryptSecretOrPlain((string)($credentials['token'] ?? ''));
    }

    private function normalizeYoulaId(string $value): string {
        $value = strtolower(trim($value));
        return preg_match('/^[a-f0-9]{24}$/', $value) === 1 ? $value : '';
    }
}
