<?php

namespace App\Services\Mail;

use App\Models\Mail\MailAccount;
use App\Models\Mail\MailAttachment;
use App\Models\Mail\MailContact;
use App\Models\Mail\MailFolder;
use App\Models\Mail\MailMessage;
use App\Models\Mail\MailRule;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

/**
 * IMAP senkronizasyonu: klasör ağacı, mesaj başlıkları, bayraklar ve gövde.
 *
 * Strateji — başlıklar toplu çekilir (liste görünümü için yeterli), gövde ve
 * ekler yalnızca mesaj ilk kez açıldığında indirilip yerelde saklanır.
 */
class MailSyncService
{
    /** Tek FETCH'te istenecek mesaj sayısı. */
    private const CHUNK = 50;

    /**
     * Bayrak eşitlemesinde geriye dönük bakılacak azami mesaj sayısı.
     *
     * Bu sınırın altındaki klasörlerde bayraklar baştan sona eşitlenir —
     * FETCH yalnızca bayrak istediği için maliyeti düşüktür. Daha büyük
     * klasörlerde en yeni FLAG_WINDOW ileti taranır.
     */
    private const FLAG_WINDOW = 2000;

    /** Bu boyutun altındaki klasörlerde bayraklar tam olarak eşitlenir. */
    private const FLAG_FULL_LIMIT = 8000;

    /** Bayrak eşitlemesinde tek FETCH'e sığdırılacak UID sayısı. */
    private const FLAG_CHUNK = 1000;

    private array $clients = [];

    public function client(MailAccount $account): ImapClient
    {
        if (isset($this->clients[$account->id])) {
            return $this->clients[$account->id];
        }

        $client = new ImapClient(
            $account->imap_host,
            (int) $account->imap_port,
            $account->imap_encryption,
            (bool) $account->imap_validate_cert,
        );

        $client->login($account->imap_username, (string) $account->imap_password);

        return $this->clients[$account->id] = $client;
    }

    public function closeAll(): void
    {
        foreach ($this->clients as $client) {
            $client->logout();
        }
        $this->clients = [];
    }

    /**
     * Hesabı senkronize eder.
     *
     * @param  MailFolder|null  $only  Yalnızca bu klasör (hızlı yenileme)
     * @param  float|null  $deadline  microtime(true) cinsinden bitiş anı; aşılırsa
     *                                kalan klasörler sonraki turda işlenir
     * @return array{folders:int,new:int,updated:int,partial:bool}
     */
    public function syncAccount(MailAccount $account, ?MailFolder $only = null, ?float $deadline = null): array
    {
        $stats = ['folders' => 0, 'new' => 0, 'updated' => 0, 'partial' => false];

        try {
            $client = $this->client($account);

            $folders = $only ? collect([$only]) : $this->syncFolders($account, $client);
            $folders = $this->prioritize($folders);
            $stats['folders'] = $folders->count();

            foreach ($folders as $folder) {
                if (! $folder->is_selectable || ! $folder->is_subscribed) {
                    continue;
                }

                // Süre bütçesi dolduysa kalanı bir sonraki senkrona bırak:
                // istek sonsuza kadar açık kalmasın, hiçbir klasör de aç kalmasın
                if ($deadline !== null && microtime(true) >= $deadline) {
                    $stats['partial'] = true;
                    break;
                }

                try {
                    $result = $this->syncFolder($account, $client, $folder);
                    $stats['new'] += $result['new'];
                    $stats['updated'] += $result['updated'];
                } catch (\Throwable $e) {
                    Log::warning('Mail klasör senkronu başarısız', [
                        'account' => $account->id, 'folder' => $folder->path, 'error' => $e->getMessage(),
                    ]);
                }
            }

            $account->forceFill([
                'status' => 'ok',
                'last_error' => null,
                'last_synced_at' => now(),
            ])->save();
        } catch (\Throwable $e) {
            $account->forceFill([
                'status' => 'error',
                'last_error' => mb_substr($e->getMessage(), 0, 500),
                'last_synced_at' => now(),
            ])->save();

            throw $e;
        }

        return $stats;
    }

    // ─────────────────────────────────────────── klasörler

    /** Sunucudaki klasör listesini yerele aynalar. */
    public function syncFolders(MailAccount $account, ImapClient $client)
    {
        $remote = $client->listFolders();
        $seenPaths = [];

        // Üst klasör bağlantısı kurabilmek için önce yolu kısa olanlar
        usort($remote, fn ($a, $b) => strlen($a['display_path']) <=> strlen($b['display_path']));

        foreach ($remote as $item) {
            $seenPaths[] = $item['path'];

            $parentId = null;
            $delimiter = $item['delimiter'];
            if ($delimiter !== '' && str_contains($item['display_path'], $delimiter)) {
                $parentDisplay = mb_substr($item['display_path'], 0, (int) mb_strrpos($item['display_path'], $delimiter));
                $parentId = MailFolder::where('account_id', $account->id)
                    ->where('path', ImapUtf7::encode($parentDisplay))
                    ->value('id');
            }

            $folder = MailFolder::firstOrNew([
                'account_id' => $account->id,
                'path' => $item['path'],
            ]);

            $folder->fill([
                'parent_id' => $parentId,
                'name' => $item['name'],
                'delimiter' => $delimiter,
                'is_selectable' => $item['selectable'],
                'is_subscribed' => true,
            ]);

            // Tip yalnızca ilk keşifte belirlenir; kullanıcı sonradan değiştirebilir
            if (! $folder->exists || $folder->type === 'custom') {
                $folder->type = $item['type'];
            }

            $folder->sort_order = MailFolder::TYPE_META[$folder->type]['order'] ?? 7;
            $folder->save();
        }

        // Sunucuda görünmeyen klasörler abonelikten düşürülür — SİLİNMEZ.
        //
        // Silme, cascade ile o klasördeki tüm yerel iletileri ve ham yedeklerini
        // de götürür; geçici bir LIST hatası ya da ayrıştırma sorunu kalıcı veri
        // kaybı demek olurdu. Boş ve gerçekten kaybolmuş klasörler temizlenir,
        // içinde ileti olanlar arşiv olarak durur.
        if ($seenPaths) {
            $missing = MailFolder::where('account_id', $account->id)
                ->whereNotIn('path', $seenPaths)
                ->get();

            foreach ($missing as $folder) {
                $hasMessages = MailMessage::where('folder_id', $folder->id)->exists();

                if ($hasMessages) {
                    $folder->forceFill(['is_subscribed' => false, 'is_selectable' => false])->save();

                    Log::info('Mail klasörü sunucuda bulunamadı, abonelikten düşürüldü', [
                        'account' => $account->id, 'folder' => $folder->path,
                    ]);

                    continue;
                }

                $folder->delete();
            }
        }

        return $this->foldersFor($account);
    }

    /**
     * Klasörleri senkron sırasına dizer.
     *
     * Süre bütçesi dolduğunda yarıda kesilebildiği için önce kullanıcının
     * gerçekten beklediği klasörler gelir: gelen kutusu, ardından uzun süredir
     * eşitlenmemiş olanlar.
     */
    private function prioritize($folders)
    {
        return $folders->sortBy(fn (MailFolder $f) => [
            $f->type === 'inbox' ? 0 : 1,
            $f->last_synced_at?->getTimestamp() ?? 0,
        ])->values();
    }

    /**
     * Hesabın yerel aynasını sıfırlar: iletiler, indirilmiş ekler ve klasör
     * imleçleri temizlenir. Sunucudaki postalara DOKUNULMAZ — bir sonraki
     * senkronda her şey baştan indirilir.
     *
     * Ayrıştırma hatası düzeltildikten sonra eski kayıtları tazelemek için.
     */
    public function resetCache(MailAccount $account): void
    {
        $paths = MailAttachment::whereHas(
            'message', fn ($q) => $q->where('account_id', $account->id)
        )->whereNotNull('disk_path')->pluck('disk_path');

        foreach ($paths as $path) {
            Storage::disk('local')->delete($path);
        }

        MailMessage::where('account_id', $account->id)->delete();

        MailFolder::where('account_id', $account->id)->update([
            'highest_uid' => 0,
            'uidvalidity' => null,
            'messages_total' => 0,
            'messages_unread' => 0,
            'last_synced_at' => null,
        ]);
    }

    /** Hesabın klasörleri — standart tipler önce, sonra alfabetik. */
    public function foldersFor(MailAccount $account)
    {
        return MailFolder::where('account_id', $account->id)
            ->orderBy('sort_order')
            ->orderBy('name')
            ->get();
    }

    // ─────────────────────────────────────────── mesajlar

    /**
     * Tek klasörü senkronize eder: yeni mesajlar + bayrak değişiklikleri +
     * sunucudan silinenlerin yerelden düşürülmesi.
     *
     * @return array{new:int,updated:int}
     */
    public function syncFolder(MailAccount $account, ImapClient $client, MailFolder $folder): array
    {
        $info = $client->select($folder->path);
        $new = 0;

        // UIDVALIDITY değiştiyse tüm yerel UID'ler geçersizdir — sıfırdan çek
        if ($folder->uidvalidity && $info['uidvalidity'] && (int) $folder->uidvalidity !== (int) $info['uidvalidity']) {
            MailMessage::where('folder_id', $folder->id)->delete();
            $folder->highest_uid = 0;
        }

        $folder->uidvalidity = $info['uidvalidity'];
        $folder->uidnext = $info['uidnext'];

        // Hangi UID'ler çekilecek?
        if ((int) $folder->highest_uid > 0) {
            $uids = $client->search('UID '.((int) $folder->highest_uid + 1).':*');
            $uids = array_filter($uids, fn ($u) => $u > (int) $folder->highest_uid);
        } else {
            $since = Carbon::now()->subDays(max(1, (int) $account->sync_days));
            $uids = $client->searchSince($since);
        }

        $existing = MailMessage::where('folder_id', $folder->id)
            ->whereIn('uid', $uids ?: [0])
            ->pluck('uid')
            ->map(fn ($v) => (int) $v)
            ->all();

        $uids = array_values(array_diff(array_map('intval', $uids), $existing));
        sort($uids);

        foreach (array_chunk($uids, self::CHUNK) as $chunk) {
            $fetched = $client->fetchHeaders($chunk);
            foreach ($fetched as $uid => $data) {
                if ($this->storeHeader($account, $folder, $uid, $data)) {
                    $new++;
                }
            }
        }

        if ($uids) {
            $folder->highest_uid = max(max($uids), (int) $folder->highest_uid);
        }

        $updated = $this->syncFlags($client, $folder);

        $folder->messages_total = MailMessage::where('folder_id', $folder->id)->count();
        $folder->messages_unread = MailMessage::where('folder_id', $folder->id)
            ->where('is_seen', false)->where('is_deleted', false)->count();
        $folder->last_synced_at = now();
        $folder->save();

        return ['new' => $new, 'updated' => $updated];
    }

    /** Başlık verisini yerel mesaja çevirir. */
    private function storeHeader(MailAccount $account, MailFolder $folder, int $uid, array $data): bool
    {
        $parsed = MimeParser::parseHeaders($data['headers'] ?? '');
        $flags = array_map('strtolower', $data['flags'] ?? []);

        $date = $parsed['date'];
        if (! $date && ! empty($data['internal_date'])) {
            try {
                $date = Carbon::parse($data['internal_date'])->setTimezone(MimeParser::appTimezone());
            } catch (\Throwable) {
                $date = null;
            }
        }

        // Yerelde önce yazılmış kopya (gönderilen posta APPEND edilemeden kaydedilmişse)
        // yeni satır açılmaz, aynı satır gerçek UID ile tazelenir.
        $message = $parsed['message_id']
            ? MailMessage::where('folder_id', $folder->id)
                ->whereNull('uid')
                ->where('message_id', $parsed['message_id'])
                ->first()
            : null;

        $attributes = [
            'account_id' => $account->id,
            'folder_id' => $folder->id,
            'uid' => $uid,
            'message_id' => $parsed['message_id'],
            'in_reply_to' => $parsed['in_reply_to'],
            'references_json' => $parsed['references'],
            'thread_key' => MailMessage::threadKeyFor($parsed['subject']),
            'subject' => mb_substr($parsed['subject'], 0, 900),
            'from_name' => mb_substr($parsed['from']['name'], 0, 190),
            'from_email' => mb_substr($parsed['from']['email'], 0, 190),
            'to_json' => $parsed['to'],
            'cc_json' => $parsed['cc'],
            'bcc_json' => $parsed['bcc'],
            'reply_to_json' => $parsed['reply_to'],
            'date_at' => $date ?: now(),
            'size' => (int) ($data['size'] ?? 0),
            'has_attachments' => $parsed['has_attachments'],
            'is_seen' => in_array('\\seen', $flags, true),
            'is_flagged' => in_array('\\flagged', $flags, true),
            'is_answered' => in_array('\\answered', $flags, true),
            'is_draft' => in_array('\\draft', $flags, true) || $folder->type === 'drafts',
            'is_deleted' => in_array('\\deleted', $flags, true),
            'priority' => $parsed['priority'],
            'headers_json' => $this->slimHeaders($parsed['headers']),
        ];

        if ($message) {
            $message->fill($attributes);
        } else {
            $message = new MailMessage($attributes);
        }

        // İlişkiler elde: aşağıdaki adımlar klasör/hesap için tekrar sorgu atmasın
        $message->setRelation('folder', $folder);
        $message->setRelation('account', $account);

        try {
            $message->save();
        } catch (\Illuminate\Database\QueryException $e) {
            // Aynı UID paralel senkronda eklenmiş olabilir (beklenen durum);
            // başka bir veritabanı hatası ise sessizce kaybolmasın.
            $duplicate = str_contains(mb_strtolower($e->getMessage()), 'duplicate')
                || in_array((string) ($e->errorInfo[1] ?? ''), ['1062'], true);

            if (! $duplicate) {
                Log::warning('Mail başlığı kaydedilemedi', [
                    'account' => $account->id,
                    'folder' => $folder->path,
                    'uid' => $uid,
                    'error' => $e->getMessage(),
                ]);
            }

            return false;
        }

        if ($folder->type === 'inbox') {
            $this->applyRules($account, $message);
        }

        // Adres defteri gelen yazışmalardan da beslenir: yazma penceresindeki
        // otomatik tamamlama yalnızca daha önce yazdıklarımızı değil, bize
        // yazanları da bilsin.
        $this->rememberContacts($account, $message);

        // CRM köprüsü: karşı taraf tanımlı bir müşteriyse ileti kartına bağlanır
        try {
            app(MailCustomerLinker::class)->linkMessage($message);
        } catch (\Throwable $e) {
            Log::warning('Mail-müşteri bağı kurulamadı', ['message' => $message->id, 'error' => $e->getMessage()]);
        }

        // Zincir takibi: bu ileti daha önce bir satışa/operasyona/karta
        // bağlanmış bir yazışmanın devamıysa aynı hedeflere kendiliğinden
        // bağlanır — kullanıcı her cevabı tek tek ilişkilendirmesin. Giden
        // kopyalar da Gönderilenler klasöründen buraya düştüğü için ayrı bir
        // gönderim kancası gerekmez. Servis kendi içinde try/catch'lidir.
        $links = app(MailLinkService::class);
        $links->autoLinkFromThread($message);

        // Gönderici tanıma: karşı taraf panelde kayıtlı bir tedarikçi ya da
        // kaynaksa ileti o kayda kendiliğinden bağlanır. Zincir bağı zaten
        // kurulduysa aynı hedefe ikinci satır açılmaz (firstOrCreate).
        // Müşteri tarafı yukarıdaki MailCustomerLinker'da kalır.
        $links->autoLinkFromSender($message);

        return true;
    }

    /** Yalnızca işe yarayan başlıkları saklar (satır şişmesin). */
    private function slimHeaders(array $headers): array
    {
        $keep = ['return-path', 'delivered-to', 'list-unsubscribe', 'x-mailer', 'user-agent', 'content-type', 'authentication-results'];

        return collect($headers)
            ->only($keep)
            ->map(fn ($v) => mb_substr((string) $v, 0, 500))
            ->all();
    }

    /**
     * Sunucudaki bayrakları yerele yansıtır ve sunucudan silinmiş mesajları düşürür.
     */
    private function syncFlags(ImapClient $client, MailFolder $folder): int
    {
        $total = MailMessage::where('folder_id', $folder->id)->whereNotNull('uid')->count();
        if ($total === 0) {
            return 0;
        }

        // Küçük ve orta klasörlerde tamamı taranır: yalnızca bayrak istendiği
        // için maliyet düşüktür ve eski iletilerdeki okundu / sunucudan silindi
        // değişiklikleri de yerele yansır. Devasa klasörlerde en yeni uçla
        // yetinilir; oradaki eskiler zaten değişmiyor.
        $window = $total <= self::FLAG_FULL_LIMIT ? $total : self::FLAG_WINDOW;

        $local = MailMessage::where('folder_id', $folder->id)
            ->whereNotNull('uid')
            ->orderByDesc('uid')
            ->limit($window)
            ->get(['id', 'uid', 'is_seen', 'is_flagged', 'is_answered', 'is_deleted']);

        if ($local->isEmpty()) {
            return 0;
        }

        // Tek FETCH'te binlerce UID istemek hem komut satırını hem sunucunun
        // yanıtını şişirir; parçalara bölünüp birleştirilir.
        $remote = [];
        foreach ($local->pluck('uid')->map(fn ($v) => (int) $v)->chunk(self::FLAG_CHUNK) as $chunk) {
            $remote += $client->fetchFlags($chunk->all());
        }

        $updated = 0;

        foreach ($local as $message) {
            $uid = (int) $message->uid;

            // Sunucuda yoksa yerelden de kalksın
            if (! array_key_exists($uid, $remote)) {
                $message->delete();
                $updated++;

                continue;
            }

            $flags = array_map('strtolower', $remote[$uid]);
            $next = [
                'is_seen' => in_array('\\seen', $flags, true),
                'is_flagged' => in_array('\\flagged', $flags, true),
                'is_answered' => in_array('\\answered', $flags, true),
                'is_deleted' => in_array('\\deleted', $flags, true),
            ];

            $changed = array_filter($next, fn ($v, $k) => (bool) $message->{$k} !== $v, ARRAY_FILTER_USE_BOTH);
            if ($changed) {
                $message->forceFill($next)->save();
                $updated++;
            }
        }

        return $updated;
    }

    // ─────────────────────────────────────────── gövde

    /**
     * Mesaj gövdesini (ve eklerini) sunucudan çeker, yerelde saklar.
     * Zaten çekilmişse doğrudan döner.
     */
    public function ensureBody(MailMessage $message, bool $force = false): MailMessage
    {
        if (! $force && $message->body_fetched_at) {
            return $message;
        }

        $account = $message->account;
        $folder = $message->folder;

        if (! $account || ! $folder || ! $message->uid) {
            return $message;
        }

        $client = $this->client($account);
        $client->select($folder->path, true);   // EXAMINE: \Seen bayrağı kendiliğinden değişmesin

        $raw = $client->fetchRaw((int) $message->uid);
        if ($raw === null) {
            return $message;
        }

        return $this->storeParsedBody($message, $raw);
    }

    /**
     * Ham MIME metnini ayrıştırıp gövdeyi ve ekleri yerele yazar.
     *
     * ensureBody bunu indirmenin hemen ardından çağırır; yedekleme servisi de
     * kendi indirdiği ham metni buraya verir — böylece aynı ileti iki kez
     * indirilmez.
     */
    public function storeParsedBody(MailMessage $message, string $raw): MailMessage
    {
        $parsed = MimeParser::parse($raw);

        DB::transaction(function () use ($message, $parsed) {
            $message->attachments()->delete();

            $realAttachments = 0;

            foreach ($parsed['attachments'] as $index => $attachment) {
                $path = $this->storeAttachment($message, $index, $attachment);

                MailAttachment::create([
                    'message_id' => $message->id,
                    'filename' => $attachment['filename'] ?: 'ek-'.($index + 1),
                    'mime' => mb_substr((string) $attachment['mime'], 0, 190),
                    'size' => (int) $attachment['size'],
                    'part_id' => mb_substr((string) $attachment['part_id'], 0, 30),
                    'content_id' => mb_substr((string) $attachment['content_id'], 0, 190) ?: null,
                    'is_inline' => (bool) $attachment['is_inline'],
                    'disk_path' => $path,
                ]);

                if (! $attachment['is_inline']) {
                    $realAttachments++;
                }
            }

            $message->forceFill([
                'body_html' => $parsed['html'],
                'body_text' => $parsed['text'],
                'snippet' => MailHtmlSanitizer::snippet($parsed['html'], $parsed['text']),
                'has_attachments' => $realAttachments > 0,
                'body_fetched_at' => now(),
            ])->save();
        });

        return $message->fresh(['attachments']);
    }

    /** Eki diske yazar, göreli yolu döner. */
    private function storeAttachment(MailMessage $message, int $index, array $attachment): ?string
    {
        $name = MimeParser::sanitizeFilename($attachment['filename'] ?: 'ek-'.($index + 1));
        $extension = pathinfo($name, PATHINFO_EXTENSION);
        $safe = Str::slug(pathinfo($name, PATHINFO_FILENAME)) ?: 'ek';
        $safe = mb_substr($safe, 0, 60).($extension ? '.'.mb_strtolower($extension) : '');

        $path = "mail/{$message->account_id}/{$message->id}/{$index}-{$safe}";

        try {
            Storage::disk('local')->put($path, $attachment['content']);
        } catch (\Throwable $e) {
            Log::warning('Mail eki kaydedilemedi', ['message' => $message->id, 'error' => $e->getMessage()]);

            return null;
        }

        return $path;
    }

    // ─────────────────────────────────────────── kurallar

    /** Spam servisi tek örnek tutulur: engel listesi senkron boyunca bir kez okunsun. */
    private ?MailSpamService $spam = null;

    private function spam(): MailSpamService
    {
        return $this->spam ??= app(MailSpamService::class);
    }

    /**
     * Gelen mesaja acente filtrelerini uygular.
     *
     * Engellenen göndericiler kullanıcı kurallarından ÖNCE denetlenir:
     * engelleme açık ve kesin bir taleptir ("bu adresten gelen hep spama
     * gitsin"), sıradan bir filtrenin onu geçersiz kılması sürpriz olurdu.
     * İleti gereksize taşındıysa kalan kurallar hiç işletilmez — ileti artık
     * gelen kutusunda değildir ve taşıma/işaretleme eylemleri anlamsızdır.
     */
    public function applyRules(MailAccount $account, MailMessage $message): void
    {
        if ($this->spam()->screen($account, $message)) {
            return;
        }

        $rules = MailRule::where('agency_id', $account->agency_id)
            ->where('is_active', true)
            ->where(fn ($q) => $q->whereNull('account_id')->orWhere('account_id', $account->id))
            ->orderBy('sort_order')
            ->orderBy('id')
            ->get();

        foreach ($rules as $rule) {
            if (! $rule->matches($message)) {
                continue;
            }

            $this->applyRuleActions($rule, $account, $message);

            if ($rule->stop_processing) {
                break;
            }
        }
    }

    /**
     * Tek bir kuralın eylemlerini iletiye uygular.
     *
     * @return bool İleti bulunduğu klasörden ayrıldıysa true (taşındı ya da silindi)
     */
    public function applyRuleActions(MailRule $rule, MailAccount $account, MailMessage $message): bool
    {
        $actions = $rule->actions_json ?: [];

        if (! empty($actions['mark_read'])) {
            $message->is_seen = true;
        }
        if (! empty($actions['flag'])) {
            $message->is_flagged = true;
        }
        $message->save();

        $actionService = app(MailActionService::class);

        // "Sil" seçiliyse ileti çöp kutusuna taşınır — kalıcı silme kuralla
        // yapılmaz: yanlış yazılmış bir filtre postayı geri dönülmez biçimde
        // yok etmesin.
        if (! empty($actions['delete'])) {
            $actionService->trash(collect([$message]));

            return true;
        }

        if (! empty($actions['move_to'])) {
            $target = is_numeric($actions['move_to'])
                ? MailFolder::where('account_id', $account->id)->find((int) $actions['move_to'])
                : $account->folderOfType((string) $actions['move_to']);

            if ($target && $target->id !== $message->folder_id) {
                $actionService->move(collect([$message]), $target);

                return true;
            }
        }

        return false;
    }

    /**
     * Kuralı halihazırda inen iletilere uygular ("Şimdi çalıştır").
     *
     * Kurallar normalde yalnızca yeni gelen postaya işler; yeni yazılan bir
     * filtrenin geçmişe de uygulanabilmesi için gelen kutusundaki son
     * iletiler taranır.
     *
     * @return int Etkilenen ileti sayısı
     */
    public function applyRuleToExisting(MailRule $rule, int $limit = 500): int
    {
        $accounts = MailAccount::where('agency_id', $rule->agency_id)
            ->when($rule->account_id, fn ($q) => $q->where('id', $rule->account_id))
            ->get();

        $affected = 0;

        foreach ($accounts as $account) {
            $inbox = $account->folderOfType('inbox');
            if (! $inbox) {
                continue;
            }

            $messages = MailMessage::where('folder_id', $inbox->id)
                ->visible()
                ->orderByDesc('date_at')
                ->limit($limit)
                ->get();

            foreach ($messages as $message) {
                $message->setRelation('folder', $inbox);
                $message->setRelation('account', $account);

                if (! $rule->matches($message)) {
                    continue;
                }

                try {
                    $this->applyRuleActions($rule, $account, $message);
                    $affected++;
                } catch (\Throwable $e) {
                    Log::warning('Kural mevcut iletiye uygulanamadı', [
                        'rule' => $rule->id, 'message' => $message->id, 'error' => $e->getMessage(),
                    ]);
                }
            }
        }

        return $affected;
    }

    /**
     * Yazışmalardan adres defterini besler.
     *
     * Gelen kutusunda gönderen, gönderilenlerde alıcılar kaydedilir. Gereksiz
     * ve çöp klasörleri atlanır — istenmeyen posta adresleri deftere girmesin.
     */
    public function rememberContacts(MailAccount $account, MailMessage $message): void
    {
        $type = $message->folder?->type;

        if (in_array($type, ['junk', 'trash'], true)) {
            return;
        }

        $agencyId = (int) $account->agency_id;
        $self = mb_strtolower((string) $account->email);

        if (in_array($type, ['sent', 'drafts'], true)) {
            foreach (array_merge($message->to_json ?: [], $message->cc_json ?: []) as $address) {
                MailContact::remember($agencyId, (string) ($address['email'] ?? ''), $address['name'] ?? null);
            }

            return;
        }

        if ($message->from_email && mb_strtolower($message->from_email) !== $self) {
            MailContact::remember($agencyId, $message->from_email, $message->from_name);
        }
    }
}
