<?php

namespace App\Http\Controllers\Agency\Mail;

use App\Http\Controllers\Controller;
use App\Models\Mail\MailAccount;
use App\Models\Mail\MailAttachment;
use App\Models\Mail\MailBlockedSender;
use App\Models\Mail\MailContact;
use App\Models\Mail\MailFolder;
use App\Models\Mail\MailMessage;
use App\Services\Mail\MailActionService;
use App\Services\Mail\MailCustomerLinker;
use App\Services\Mail\MailDirectory;
use App\Services\Mail\MailHtmlSanitizer;
use App\Services\Mail\MailLinkService;
use App\Services\Mail\MailSenderResolver;
use App\Services\Mail\MailSendService;
use App\Services\Mail\MailSpamService;
use App\Services\Mail\MailSyncService;
use App\Services\Mail\MimeParser;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\HeaderUtils;
use Symfony\Component\HttpFoundation\StreamedResponse;

/**
 * Mail modülü — Outlook benzeri üç bölmeli e-posta istemcisi.
 *
 * Sayfa yereldeki aynadan anında açılır; IMAP senkronu arka planda AJAX ile
 * tetiklenir (yenile düğmesi / periyodik). Tüm listeleme ve arama yerel
 * veritabanından yapılır, tüm değiştirme işlemleri IMAP'e yazılır.
 */
class MailController extends Controller
{
    public function __construct(
        private MailSyncService $sync,
        private MailActionService $actions,
        private MailSendService $sender,
    ) {}

    // ─────────────────────────────────────────── erişim kapsamı

    private function agencyId(): int
    {
        return (int) (Auth::user()?->currentAgencyId() ?? 0);
    }

    /** Kullanıcının görebileceği hesaplar: acente geneli + kendi kişisel hesapları. */
    private function accountsQuery()
    {
        $userId = (int) Auth::id();

        return MailAccount::where('agency_id', $this->agencyId())
            ->where(fn ($q) => $q->whereNull('user_id')->orWhere('user_id', $userId))
            ->orderBy('sort_order')
            ->orderBy('id');
    }

    private function accounts()
    {
        return $this->accountsQuery()->with('identities')->get();
    }

    private function accountIds(): array
    {
        return $this->accountsQuery()->pluck('id')->map(fn ($v) => (int) $v)->all();
    }

    private function findAccount(int $id): MailAccount
    {
        $account = $this->accountsQuery()->find($id);
        abort_if(! $account, 404, 'E-posta hesabı bulunamadı.');

        return $account;
    }

    private function findFolder(int $id): MailFolder
    {
        $folder = MailFolder::whereIn('account_id', $this->accountIds())->with('account')->find($id);
        abort_if(! $folder, 404, 'Klasör bulunamadı.');

        return $folder;
    }

    /** İstekteki id listesinden erişilebilir mesajları toplar. */
    private function messagesFromRequest(Request $request)
    {
        $ids = collect($request->input('ids', []))->map(fn ($v) => (int) $v)->filter()->unique();
        if ($ids->isEmpty() && $request->filled('id')) {
            $ids = collect([(int) $request->input('id')]);
        }

        return MailMessage::whereIn('id', $ids)
            ->whereIn('account_id', $this->accountIds())
            ->with(['folder', 'account'])
            ->get();
    }

    // ─────────────────────────────────────────── sayfa

    public function index(Request $request)
    {
        $accounts = $this->accounts();
        $tree = $this->folderTree($accounts);

        return view('agency.mail.index', [
            'accounts' => $accounts,
            'tree' => $tree,
            'pinned' => collect($tree)->flatten()->filter(fn (MailFolder $f) => $f->is_pinned)->values(),
            'unifiedCounts' => $this->unifiedCounts(),
            'hasAccounts' => $accounts->isNotEmpty(),
            // İstemci tarafı için sadeleştirilmiş veri (window.MailConfig)
            'jsAccounts' => $accounts->map(fn (MailAccount $a) => [
                'id' => $a->id,
                'name' => $a->name,
                'email' => $a->email,
                'color' => $a->color,
                'is_default' => (bool) $a->is_default,
                'has_signature' => filled($a->signature_html),
                // Profil seçilmediğinde "Kimden" listesinde görünecek ad
                'display_from' => $a->displayFrom(),
                // Gönderici profilleri: aynı adresten farklı görünen adlarla yazışma.
                // İmza bayrakları burada da taşınır ki yazma penceresindeki "İmza"
                // düğmesi imza olup olmadığını sormak için sunucuya gitmesin.
                'identities' => $a->identities->map(fn ($i) => [
                    'id' => $i->id,
                    'name' => $i->name,
                    'is_default' => (bool) $i->is_default,
                    'has_signature' => filled($i->signature_html),
                    'inherits_signature' => $i->signature_html === null,
                ])->values(),
            ])->values(),
            'jsFolders' => collect($tree)->map(fn ($list) => collect($list)->map(fn (MailFolder $f) => [
                'id' => $f->id,
                'account_id' => $f->account_id,
                'name' => $f->label(),
                'type' => $f->type,
                'icon' => $f->icon(),
            ])->values()),
        ]);
    }

    /** Hesap → klasör listesi (üst/alt sıralı). */
    private function folderTree($accounts): array
    {
        $folders = MailFolder::whereIn('account_id', $accounts->pluck('id'))
            // Sabitlenmiş klasörler hesap adını da gösteriyor
            ->with('account:id,name')
            ->orderBy('sort_order')
            ->orderBy('name')
            ->get()
            ->groupBy('account_id');

        $tree = [];
        foreach ($accounts as $account) {
            $tree[$account->id] = ($folders[$account->id] ?? collect())->values();
        }

        return $tree;
    }

    /** "Tüm Hesaplar" başlığındaki birleşik sayaçlar. */
    private function unifiedCounts(): array
    {
        $accountIds = $this->accountIds();
        if (! $accountIds) {
            return [];
        }

        $rows = MailFolder::whereIn('account_id', $accountIds)
            ->selectRaw('type, SUM(messages_total) as total, SUM(messages_unread) as unread')
            ->groupBy('type')
            ->get();

        $out = [];
        foreach ($rows as $row) {
            $out[$row->type] = ['total' => (int) $row->total, 'unread' => (int) $row->unread];
        }

        return $out;
    }

    // ─────────────────────────────────────────── liste

    /** Mesaj listesi (JSON). Klasör, birleşik tip, arama ve filtre destekler. */
    public function messages(Request $request): JsonResponse
    {
        $accountIds = $this->accountIds();
        if (! $accountIds) {
            return response()->json(['messages' => [], 'has_more' => false, 'total' => 0]);
        }

        $query = MailMessage::query()
            ->whereIn('mail_messages.account_id', $accountIds)
            ->visible()
            ->with(['folder:id,type,name,account_id', 'account:id,name,email,color'])
            // Liste satırındaki "ilişkili" ipucu. Adresten tanınan gönderici de
            // senkron sırasında bağ ürettiği için bu sayaç tanınmayı da yansıtır.
            // Satır başına gönderici çözümlemesi BİLEREK yapılmıyor: bir sayfa
            // 100 iletiye kadar çıkıyor ve adres eşleşmesi müşteri/tedarikçi
            // JSON kolonlarında LIKE taraması demek — liste bunun altında
            // gözle görülür biçimde yavaşlardı. withCount tek ek sorgudur.
            ->when(app(MailLinkService::class)->ready(), fn ($q) => $q->withCount('links'));

        // Kapsam: tek klasör · birleşik tip · tüm hesaplarda tip
        if ($request->filled('folder_id')) {
            $folder = $this->findFolder((int) $request->input('folder_id'));
            $query->where('mail_messages.folder_id', $folder->id);
        } elseif ($request->filled('unified')) {
            $type = (string) $request->input('unified');
            $folderIds = MailFolder::whereIn('account_id', $accountIds)->where('type', $type)->pluck('id');
            $query->whereIn('mail_messages.folder_id', $folderIds);
        } else {
            $folderIds = MailFolder::whereIn('account_id', $accountIds)->where('type', 'inbox')->pluck('id');
            $query->whereIn('mail_messages.folder_id', $folderIds);
        }

        // Hızlı filtreler
        match ($request->input('filter')) {
            'unread' => $query->where('mail_messages.is_seen', false),
            'flagged' => $query->where('mail_messages.is_flagged', true),
            'attachments' => $query->where('mail_messages.has_attachments', true),
            default => null,
        };

        // Arama
        if ($term = trim((string) $request->input('q'))) {
            MailMessage::applySearch($query, $term);
        }

        $perPage = min(100, max(10, (int) $request->input('per_page', 50)));
        $page = max(1, (int) $request->input('page', 1));

        $total = (clone $query)->count();
        $messages = $query->orderByDesc('mail_messages.date_at')
            ->orderByDesc('mail_messages.id')
            ->forPage($page, $perPage)
            ->get();

        return response()->json([
            'messages' => $messages->map(fn (MailMessage $m) => $this->listPayload($m))->all(),
            'total' => $total,
            'page' => $page,
            'has_more' => $page * $perPage < $total,
        ]);
    }

    private function listPayload(MailMessage $m): array
    {
        $party = $m->counterparty();

        return [
            'id' => $m->id,
            'account_id' => $m->account_id,
            'account_name' => $m->account?->name,
            'account_color' => $m->account?->color ?? '#2563eb',
            'folder_id' => $m->folder_id,
            'folder_type' => $m->folder?->type,
            'folder_name' => $m->folder?->label() ?? '',
            'subject' => $m->subject ?: '(konu yok)',
            'snippet' => $m->snippet ?: '',
            'party_name' => $party['name'],
            'party_email' => $party['email'],
            'initial' => $m->initial(),
            'date_iso' => optional($m->date_at)->toIso8601String(),
            'date_ts' => optional($m->date_at)->getTimestamp(),
            'time_label' => $this->timeLabel($m->date_at),
            'group_label' => $this->groupLabel($m->date_at),
            'is_seen' => (bool) $m->is_seen,
            'is_flagged' => (bool) $m->is_flagged,
            'is_answered' => (bool) $m->is_answered,
            'is_draft' => (bool) $m->is_draft,
            'has_attachments' => (bool) $m->has_attachments,
            'priority' => (int) $m->priority,
            // Panelde bir kayda bağlı mı? (elle · zincirden · göndericiden)
            // Müşteri bağı eski yoldan geldiği için ayrıca sorulur.
            'is_linked' => ((int) ($m->links_count ?? 0)) > 0 || $m->customer_id !== null,
        ];
    }

    /** Bugün ise saat, bu yıl ise "12 Oca", değilse tam tarih. */
    private function timeLabel(?Carbon $date): string
    {
        if (! $date) {
            return '';
        }
        if ($date->isToday()) {
            return $date->format('H:i');
        }
        if ($date->isYesterday()) {
            return 'Dün';
        }
        if ($date->isSameYear(now())) {
            return $date->translatedFormat('d M');
        }

        return $date->format('d.m.Y');
    }

    /** Liste içinde tarih başlığı ("Bugün", "Dün", "Bu Hafta"…). */
    private function groupLabel(?Carbon $date): string
    {
        if (! $date) {
            return 'Tarihsiz';
        }
        if ($date->isToday()) {
            return 'Bugün';
        }
        if ($date->isYesterday()) {
            return 'Dün';
        }
        if ($date->greaterThanOrEqualTo(now()->startOfWeek())) {
            return 'Bu Hafta';
        }
        if ($date->greaterThanOrEqualTo(now()->startOfWeek()->subWeek())) {
            return 'Geçen Hafta';
        }
        if ($date->isSameMonth(now())) {
            return 'Bu Ay';
        }

        return $date->translatedFormat('F Y');
    }

    // ─────────────────────────────────────────── okuma

    /** Tek mesaj: gövde ilk açılışta IMAP'ten indirilir. */
    public function show(Request $request, int $id): JsonResponse
    {
        $message = MailMessage::whereIn('account_id', $this->accountIds())
            ->with(['folder', 'account', 'attachments'])
            ->findOrFail($id);

        $error = null;
        if (! $message->body_fetched_at) {
            try {
                $message = $this->sync->ensureBody($message);
                $message->load(['folder', 'account', 'attachments']);
            } catch (\Throwable $e) {
                $error = 'Mesaj içeriği sunucudan alınamadı: '.$e->getMessage();
            }
        }

        // Okundu işaretle (taslak değilse ve zaten okunmadıysa)
        if (! $message->is_seen && $request->boolean('mark_read', true)) {
            try {
                $this->actions->setSeen(collect([$message]), true);
                $message->is_seen = true;
            } catch (\Throwable) {
                $message->forceFill(['is_seen' => true])->save();
            }
        }

        $allowRemote = $request->boolean('show_images');
        $bodyHtml = $message->body_html
            ? MailHtmlSanitizer::clean($message->body_html, $message, $allowRemote)
            : MailHtmlSanitizer::fromText($message->body_text);

        return response()->json([
            'id' => $message->id,
            'account_id' => $message->account_id,
            'account_email' => $message->account?->email,
            'account_name' => $message->account?->name,
            'folder_id' => $message->folder_id,
            'folder_type' => $message->folder?->type,
            'subject' => $message->subject ?: '(konu yok)',
            'from' => ['name' => $message->from_name, 'email' => $message->from_email],
            'to' => $message->to_json ?: [],
            'cc' => $message->cc_json ?: [],
            'bcc' => $message->bcc_json ?: [],
            'reply_to' => $message->reply_to_json ?: [],
            'date_full' => optional($message->date_at)->translatedFormat('d F Y, H:i'),
            'date_rel' => optional($message->date_at)->diffForHumans(),
            'is_seen' => (bool) $message->is_seen,
            'is_flagged' => (bool) $message->is_flagged,
            'is_answered' => (bool) $message->is_answered,
            'is_draft' => (bool) $message->is_draft,
            'priority' => (int) $message->priority,
            'initial' => $message->initial(),
            'body_html' => $bodyHtml,
            'has_remote_images' => ! $allowRemote && MailHtmlSanitizer::hasRemoteImages($message->body_html),
            'attachments' => $message->attachments->where('is_inline', false)->values()->map(fn (MailAttachment $a) => [
                'id' => $a->id,
                'filename' => $a->filename,
                'mime' => $a->mime,
                'size' => $a->humanSize(),
                'icon' => $a->icon(),
                'is_image' => (bool) preg_match('#^image/#i', (string) $a->mime),
                'url' => route('agency.mail.attachments.show', $a->id),
                'download_url' => route('agency.mail.attachments.download', $a->id),
            ])->all(),
            'reply' => $this->replyContext($message),
            'crm' => $this->crmContext($message),
            'error' => $error,
        ]);
    }

    /**
     * CRM durumu: iletiye bağlı kayıtlar (müşteri · satış · operasyon ·
     * tedarikçi · kaynak), yoksa yeni kayıt açmak için ön doldurulmuş alanlar.
     *
     * İki bağ türü bir arada gösterilir: adres eşleşmesiyle kurulan müşteri
     * bağı (mail_messages.customer_id, eski davranış) ve elle/zincirle kurulan
     * çok yönlü bağlar (mail_links). Kullanıcı açısından ikisi de "bu ileti şu
     * kayda ait" demek; ayrımı yalnızca kaldırma davranışı belirler.
     */
    private function crmContext(MailMessage $message): array
    {
        $linker = app(MailCustomerLinker::class);
        $service = app(MailLinkService::class);

        // Bağ yoksa adres eşleşmesi bir kez daha denenir: müşteri, ileti
        // senkronlandıktan sonra açılmış olabilir.
        $customer = $message->customer ?: $linker->linkMessage($message);

        $links = $service->linksPayload($message);

        // Adres eşleşmesinden gelen müşteri, elle kurulmuş bağlar arasında
        // yoksa şeride ayrıca eklenir. `link_id` yoktur: bu bağ mail_links'te
        // durmadığı için şeritten kaldırılamaz, yalnızca gösterilir.
        $alreadyListed = collect($links)
            ->contains(fn ($l) => $l['type'] === 'customer' && $l['id'] === (int) $customer?->id);

        if ($customer && ! $alreadyListed) {
            $info = $service->describe($customer);
            $info['link_id'] = null;
            $info['is_auto'] = true;
            // Adresten türeyen bağ: çip "tanınan gönderici" hâlinde çizilir
            $info['source'] = \App\Models\Mail\MailLink::SOURCE_ADDRESS;
            $info['is_address'] = true;
            array_unshift($links, $info);
        }

        $emails = $linker->counterpartyEmails($message);
        $name = $linker->splitName($message->from_name ?: ($emails[0] ?? ''));

        // Gönderici tanıma: adres panelde kayıtlıysa (müşteri · tedarikçi ·
        // yetkili · kaynak · personel) künyesi şeritte rozet olarak görünür.
        // Bağ kurulamayan kaynaklar (personel) için tek görünür iz budur.
        $sender = app(MailSenderResolver::class)->resolve($this->agencyId(), $emails);

        return [
            // Eski alanlar korunur — okuma bölmesi dışındaki çağıranlar bozulmasın
            'linked' => (bool) $customer,
            'customer_id' => $customer?->id,
            'name' => $customer?->full_name,
            'code' => $customer?->code,
            'initials' => $customer?->initials,
            'url' => $customer ? route('agency.customers.show', $customer) : null,

            'links' => $links,
            'sender' => $sender ? [
                'kind' => $sender['kind'],
                'kind_label' => $sender['kind_label'],
                'icon' => $sender['icon'],
                'color' => $sender['color'],
                'name' => $sender['name'],
                'meta' => $sender['meta'],
                'email' => $sender['email'],
                // Bağ kurulabilen kaynaklarda çip zaten var; rozet ek bilgidir
                'linkable' => $sender['link_type'] !== null,
            ] : null,
            'types' => $this->linkTypesPayload(MailLinkService::PICKER_TYPES),
            // "İlk defa yazan kişi" hâli: ileti hiçbir kayda bağlı DEĞİLSE ve
            // gönderici de panelde tanınmıyorsa kayıt açma daveti öne çıkar.
            // Tanınan bir gönderici için ikinci bir kart açmak öneril(e)mez —
            // panel kendi personeli ya da mevcut tedarikçisi için "müşteri
            // kartı aç" demiş olurdu.
            'can_create' => empty($links) && ! $sender && ! empty($emails),
            'record_types' => $this->linkTypesPayload(array_keys(self::RECORD_TYPES)),
            'email' => $emails[0] ?? '',
            'first_name' => $name['first'],
            'last_name' => $name['last'],
            'company_name' => $this->guessCompany($message, $emails),
        ];
    }

    /** Seçici/kayıt-açma sekmeleri için tip künyeleri. */
    private function linkTypesPayload(array $keys): array
    {
        return collect($keys)
            ->filter(fn ($k) => isset(MailLinkService::TYPES[$k]))
            ->map(fn ($k) => [
                'key' => $k,
                'label' => MailLinkService::TYPES[$k]['label'],
                'icon' => MailLinkService::TYPES[$k]['icon'],
                'color' => MailLinkService::TYPES[$k]['color'],
            ])
            ->values()
            ->all();
    }

    /**
     * Gönderen adı bir firma adı gibi görünüyorsa ön doldurma için döner.
     *
     * Adres yerine ad yazan kurumsal gönderenlerde ("Otel Rezervasyon") bu
     * alan tedarikçi kartının firma adına doğrudan geçer.
     */
    private function guessCompany(MailMessage $message, array $emails): string
    {
        $from = trim((string) $message->from_name);

        if ($from === '' || filter_var($from, FILTER_VALIDATE_EMAIL)) {
            return '';
        }

        return in_array(mb_strtolower($from), array_map('mb_strtolower', $emails), true) ? '' : $from;
    }

    /**
     * Gelen iletinin göndereninden CRM müşteri kartı açar.
     *
     * Eski uç (POST .../customer) korunuyor: dışarıdan çağıran olabilir.
     * Gövde artık genelleştirilmiş storeRecord()'a devredilir — "ilk mailden
     * kayıt aç" akışı tek koddan yürüsün.
     */
    public function storeCustomer(Request $request, int $id): JsonResponse
    {
        $request->merge(['type' => 'customer']);

        return $this->storeRecord($request, $id);
    }

    /**
     * Gelen iletiden panelde yeni kayıt açar ve iletiyi o kayda bağlar.
     *
     * Hangi tiplerin desteklendiği RECORD_TYPES ile sınırlıdır: bir e-postadan
     * anlamlı biçimde açılabilecek kayıtlar müşteri, tedarikçi ve kaynaktır.
     * Satış ve operasyon buraya girmez — ikisi de e-postada bulunmayan
     * (ürün, tarih, kapasite) zorunlu bilgiler ister; onlar için kendi
     * modüllerinde kayıt açılıp ileti "İlişkilendir" ile bağlanır.
     */
    private const RECORD_TYPES = [
        'customer' => 'Müşteri',
        'supplier' => 'Tedarikçi',
        'resource' => 'Kaynak',
    ];

    public function storeRecord(Request $request, int $id): JsonResponse
    {
        $message = MailMessage::whereIn('account_id', $this->accountIds())
            ->with(['folder', 'account', 'customer'])
            ->findOrFail($id);

        $data = $request->validate([
            'type' => 'required|in:'.implode(',', array_keys(self::RECORD_TYPES)),
            'first_name' => 'nullable|string|max:100',
            'last_name' => 'nullable|string|max:100',
            'company_name' => 'nullable|string|max:190',
            'phone' => 'nullable|string|max:30',
            'customer_type' => 'nullable|in:individual,corporate',
            'supplier_type' => 'nullable|in:hotel,transportation,restaurant,insurance,visa,airline',
            'resource_type' => 'nullable|in:guide,vehicle,captain',
        ]);

        $linker = app(MailCustomerLinker::class);
        $emails = $linker->counterpartyEmails($message);

        if (! $emails) {
            return response()->json(['ok' => false, 'message' => 'İletide kullanılabilir bir e-posta adresi yok.'], 422);
        }

        if ($data['type'] === 'customer' && $message->customer) {
            return response()->json([
                'ok' => false,
                'message' => 'Bu adres zaten "'.$message->customer->full_name.'" kartına bağlı.',
            ], 422);
        }

        try {
            $record = match ($data['type']) {
                'customer' => $linker->createCustomerFromMessage($message, $data),
                'supplier' => $this->createSupplierFromMessage($message, $data, $emails[0]),
                'resource' => $this->createResourceFromMessage($message, $data, $emails[0]),
            };
        } catch (\Throwable $e) {
            return response()->json([
                'ok' => false,
                'message' => self::RECORD_TYPES[$data['type']].' kaydı açılamadı: '.$e->getMessage(),
            ], 422);
        }

        // Yeni kayıt, gönderici tanımanın önbelleğindeki "tanınmıyor" sonucunu
        // geçersiz kılar — aşağıdaki crmContext() rozeti doğru göstersin.
        app(MailSenderResolver::class)->flush();

        // Kayıt açma da ilişkilendirme mekanizmasını kullanır: iki ayrı yol
        // olmasın, açılan kayıt akışında bu yazışmayı (ve zincirini) görsün.
        app(MailLinkService::class)->attach($message, $record, (int) Auth::id());

        $info = app(MailLinkService::class)->describe($record);

        return response()->json([
            'ok' => true,
            'message' => $info['name'].' '.mb_strtolower(self::RECORD_TYPES[$data['type']]).' olarak eklendi.',
            'crm' => $this->crmContext($message->fresh(['folder', 'account', 'customer'])),
            'record' => $info,
        ]);
    }

    /** İletinin karşı tarafından tedarikçi kartı açar. */
    private function createSupplierFromMessage(MailMessage $message, array $data, string $email): \App\Models\Supplier\Supplier
    {
        $linker = app(MailCustomerLinker::class);
        $name = $linker->splitName($message->from_name ?: $email);
        $company = trim((string) ($data['company_name'] ?? ''));

        // Firma adı verilmişse kurumsal, verilmemişse kişisel tedarikçi
        $type = $company !== '' ? 'corporate' : 'individual';
        $supplierType = $data['supplier_type'] ?? 'hotel';

        $supplier = \App\Models\Supplier\Supplier::create([
            'agency_id' => $this->agencyId(),
            'type' => $type,
            'supplier_type' => $supplierType,
            'first_name' => trim((string) ($data['first_name'] ?? '')) ?: $name['first'],
            'last_name' => trim((string) ($data['last_name'] ?? '')) ?: $name['last'],
            'company_name' => $company ?: null,
            'phone' => $data['phone'] ?? null,
            'emails' => [$email],
            'status' => 'potential',
            'created_by' => Auth::id(),
        ]);

        // Tedarikçi tipi ayrıca çoklu tabloda tutuluyor (kart bunu okuyor)
        \Illuminate\Support\Facades\DB::table('supplier_supplier_type')->insertOrIgnore([
            'supplier_id' => $supplier->id,
            'supplier_type' => $supplierType,
            'created_at' => now(),
            'updated_at' => now(),
        ]);

        return $supplier;
    }

    /** İletinin karşı tarafından kaynak (rehber / araç / kaptan) kaydı açar. */
    private function createResourceFromMessage(MailMessage $message, array $data, string $email): \App\Models\Resource
    {
        $linker = app(MailCustomerLinker::class);
        $name = $linker->splitName($message->from_name ?: $email);

        $first = trim((string) ($data['first_name'] ?? '')) ?: $name['first'];
        $last = trim((string) ($data['last_name'] ?? '')) ?: $name['last'];

        return \App\Models\Resource::create([
            'agency_id' => $this->agencyId(),
            'type' => $data['resource_type'] ?? 'guide',
            'source_type' => 'internal',
            'name' => trim($first.' '.$last) ?: $email,
            'first_name' => $first,
            'last_name' => $last,
            'email' => $email,
            'phone' => $data['phone'] ?? null,
            // Kayıt e-postadan doğuyor: eksik alanlar tamamlanana kadar
            // planlamaya çıkmasın (emsal: ResourceController::quickCreate)
            'is_active' => false,
            'created_by' => Auth::id(),
        ]);
    }

    // ─────────────────────────────────────────── ilişkilendirme

    /** İlişkilendirme seçicisinin arama ucu (müşteri · satış · operasyon · tedarikçi). */
    public function linkSearch(Request $request): JsonResponse
    {
        $request->validate([
            'type' => 'required|in:'.implode(',', array_keys(MailLinkService::TYPES)),
            'q' => 'nullable|string|max:120',
        ]);

        return response()->json([
            'items' => app(MailLinkService::class)->search(
                $this->agencyId(),
                (string) $request->input('type'),
                (string) $request->input('q', ''),
                12
            ),
        ]);
    }

    /** İletiyi bir kayda bağlar; aynı zincirdeki geçmiş iletiler de bağlanır. */
    public function storeLink(Request $request, int $message): JsonResponse
    {
        $request->validate([
            'type' => 'required|in:'.implode(',', array_keys(MailLinkService::TYPES)),
            'id' => 'required|integer|min:1',
        ]);

        $mail = MailMessage::whereIn('account_id', $this->accountIds())
            ->with(['folder', 'account', 'customer'])
            ->findOrFail($message);

        $service = app(MailLinkService::class);

        $record = $service->resolve(
            (string) $request->input('type'),
            (int) $request->input('id'),
            $this->agencyId()
        );

        if (! $record) {
            return response()->json(['ok' => false, 'message' => 'Kayıt bulunamadı ya da bu acenteye ait değil.'], 422);
        }

        $result = $service->attach($mail, $record, (int) Auth::id());
        $info = $service->describe($record);

        $note = $result['thread'] > 0
            ? ' Aynı yazışmadaki '.$result['thread'].' ileti de akışa eklendi.'
            : '';

        return response()->json([
            'ok' => true,
            'message' => $info['name'].' kaydıyla ilişkilendirildi.'.$note,
            'crm' => $this->crmContext($mail),
        ]);
    }

    /** Bağı kaldırır (elle kurulmuşsa zincirden türeyen kardeşleriyle birlikte). */
    public function destroyLink(int $message, int $link): JsonResponse
    {
        $mail = MailMessage::whereIn('account_id', $this->accountIds())
            ->with(['folder', 'account', 'customer'])
            ->findOrFail($message);

        $row = \App\Models\Mail\MailLink::where('agency_id', $this->agencyId())
            ->where('message_id', $mail->id)
            ->findOrFail($link);

        app(MailLinkService::class)->detach($row);

        return response()->json([
            'ok' => true,
            'message' => 'İlişki kaldırıldı.',
            'crm' => $this->crmContext($mail),
        ]);
    }

    /**
     * Kart akışlarındaki satır içi okuyucu.
     *
     * Müşteri, satış ve tedarikçi quick-view'larındaki etkinlik akışı, posta
     * satırına tıklandığında gövdeyi buradan ister. Kapsam mail modülüyle
     * aynıdır (erişilebilir hesaplar); okundu işareti KONMAZ — kart akışında
     * göz gezdirmek postayı okumuş saymamalı.
     */
    public function preview(int $id): JsonResponse
    {
        $message = MailMessage::whereIn('account_id', $this->accountIds())
            ->with(['folder', 'account', 'attachments'])
            ->findOrFail($id);

        $error = null;
        if (! $message->body_fetched_at) {
            try {
                $message = $this->sync->ensureBody($message);
                $message->load(['folder', 'account', 'attachments']);
            } catch (\Throwable $e) {
                $error = 'İleti içeriği sunucudan alınamadı: '.$e->getMessage();
            } finally {
                $this->sync->closeAll();
            }
        }

        return response()->json([
            'id' => $message->id,
            'subject' => $message->subject ?: '(konu yok)',
            'direction' => $message->direction(),
            'from' => ['name' => $message->from_name, 'email' => $message->from_email],
            'to' => collect($message->to_json ?: [])->pluck('email')->filter()->implode(', '),
            'cc' => collect($message->cc_json ?: [])->pluck('email')->filter()->implode(', '),
            'date_full' => optional($message->date_at)->translatedFormat('d F Y, H:i'),
            'body_html' => $message->body_html
                ? MailHtmlSanitizer::clean($message->body_html, $message, false)
                : MailHtmlSanitizer::fromText($message->body_text),
            'attachments' => $message->attachments->where('is_inline', false)->values()
                ->map(fn (MailAttachment $a) => [
                    'filename' => $a->filename,
                    'size' => $a->humanSize(),
                    'icon' => $a->icon(),
                    'download_url' => route('agency.mail.attachments.download', $a->id),
                ])->all(),
            'reply' => [
                'to' => $message->direction() === 'inbound'
                    ? $message->from_email
                    : collect($message->to_json ?: [])->pluck('email')->filter()->implode(', '),
                'subject' => 'Ynt: '.preg_replace('/^(\s*(re|ynt|fwd|fw|ilt)\s*:\s*)+/iu', '', (string) $message->subject),
                'account_id' => $message->account_id,
                'in_reply_to' => $message->message_id,
                'references' => implode(' ', array_slice(
                    array_merge($message->references_json ?: [], array_filter([$message->message_id])), -20
                )),
            ],
            'error' => $error,
        ]);
    }

    /** Yanıtla / Tümünü yanıtla / İlet için hazır veri. */
    private function replyContext(MailMessage $message): array
    {
        $account = $message->account;
        $self = mb_strtolower((string) $account?->email);

        $from = $message->reply_to_json ?: [['name' => $message->from_name, 'email' => $message->from_email]];
        $from = array_values(array_filter($from, fn ($a) => ! empty($a['email'])));

        $allRecipients = collect(array_merge($from, $message->to_json ?: [], $message->cc_json ?: []))
            ->filter(fn ($a) => ! empty($a['email']) && mb_strtolower($a['email']) !== $self)
            ->unique(fn ($a) => mb_strtolower($a['email']))
            ->values();

        $replyAllCc = $allRecipients
            ->reject(fn ($a) => collect($from)->contains(fn ($f) => mb_strtolower($f['email']) === mb_strtolower($a['email'])))
            ->values();

        $subject = (string) $message->subject;
        $stripped = preg_replace('/^(\s*(re|ynt|fwd|fw|ilt)\s*:\s*)+/iu', '', $subject) ?? $subject;

        return [
            'to' => $from,
            'cc_all' => $replyAllCc->all(),
            'subject_reply' => 'Ynt: '.$stripped,
            'subject_forward' => 'İlt: '.$stripped,
            'quote_html' => $this->quoteHtml($message),
            'in_reply_to' => $message->message_id,
            'references' => array_slice(array_merge($message->references_json ?: [], array_filter([$message->message_id])), -20),
            'attachment_ids' => $message->attachments->where('is_inline', false)->pluck('id')->all(),
        ];
    }

    /** Yanıt gövdesine eklenecek alıntı bloğu. */
    private function quoteHtml(MailMessage $message): string
    {
        $body = $message->body_html
            ? MailHtmlSanitizer::clean($message->body_html, $message, true)
            : MailHtmlSanitizer::fromText($message->body_text);

        $meta = collect([
            'Kimden' => trim(($message->from_name ?: '').' <'.($message->from_email ?: '').'>'),
            'Tarih' => optional($message->date_at)->translatedFormat('d F Y H:i'),
            'Kime' => collect($message->to_json ?: [])->pluck('email')->implode(', '),
            'Konu' => $message->subject,
        ])->filter(fn ($v) => trim((string) $v) !== '' && trim((string) $v) !== '<>')
            ->map(fn ($v, $k) => '<b>'.$k.':</b> '.e($v))
            ->implode('<br>');

        return '<br><br><div style="border-left:3px solid #d5dbe6;padding-left:14px;margin-left:2px;color:#475569">'
            .'<div style="font-size:12px;color:#64748b;margin-bottom:10px">'.$meta.'</div>'
            .$body.'</div>';
    }

    // ─────────────────────────────────────────── senkron

    /**
     * İstek içindeki senkron için ayrılan azami süre (saniye).
     *
     * Senkron tarayıcı beklerken çalıştığı için sınırsız olamaz. Bütçe
     * dolduğunda kalan klasörler yarıda bırakılır ve yanıt "partial" döner;
     * geri kalanı ya bir sonraki yenilemede ya da zamanlanmış `mail:sync`
     * görevinde tamamlanır. Klasörler en eski eşitlenenden başlanarak
     * sıralandığı için hiçbir klasör aç kalmaz.
     */
    private const SYNC_BUDGET = 45;

    /** IMAP senkronu — tüm hesaplar ya da tek hesap/klasör. */
    public function sync(Request $request): JsonResponse
    {
        // İlk senkron yüzlerce başlık indirebilir; varsayılan 30 sn yetmez
        @set_time_limit(300);

        $accounts = $request->filled('account_id')
            ? collect([$this->findAccount((int) $request->input('account_id'))])
            : $this->accounts()->where('is_active', true)->where('sync_enabled', true);

        // Tek klasör istendiyse bütçeye gerek yok: iş zaten dar kapsamlı
        $folder = $request->filled('folder_id') ? $this->findFolder((int) $request->input('folder_id')) : null;
        $deadline = $folder ? null : microtime(true) + self::SYNC_BUDGET;

        $stats = ['new' => 0, 'updated' => 0];
        $errors = [];
        $partial = false;

        foreach ($accounts as $account) {
            try {
                $scope = $folder && $folder->account_id === $account->id ? $folder : null;

                // Klasör başka bir hesaba aitse o hesap bu turda atlanır
                if ($folder && ! $scope) {
                    continue;
                }

                $result = $this->sync->syncAccount($account, $scope, $deadline);
                $stats['new'] += $result['new'];
                $stats['updated'] += $result['updated'];
                $partial = $partial || $result['partial'];
            } catch (\Throwable $e) {
                $errors[] = $account->email.': '.$e->getMessage();
            }
        }

        $this->sync->closeAll();

        return response()->json([
            'ok' => empty($errors),
            'new' => $stats['new'],
            'updated' => $stats['updated'],
            'partial' => $partial,
            'errors' => $errors,
            'folders' => $this->folderCounts(),
            'synced_at' => now()->format('H:i'),
        ]);
    }

    /** Kenar çubuğundaki sayaçları tazelemek için. */
    public function folderCounts(): array
    {
        $folders = MailFolder::whereIn('account_id', $this->accountIds())
            ->get(['id', 'account_id', 'type', 'messages_total', 'messages_unread']);

        return [
            'by_id' => $folders->mapWithKeys(fn ($f) => [$f->id => [
                'total' => (int) $f->messages_total,
                'unread' => (int) $f->messages_unread,
            ]])->all(),
            'unified' => $this->unifiedCounts(),
        ];
    }

    public function counts(): JsonResponse
    {
        return response()->json($this->folderCounts());
    }

    // ─────────────────────────────────────────── işlemler

    public function markRead(Request $request): JsonResponse
    {
        return $this->runAction(fn () => $this->actions->setSeen($this->messagesFromRequest($request), $request->boolean('seen', true)));
    }

    public function toggleFlag(Request $request): JsonResponse
    {
        return $this->runAction(fn () => $this->actions->setFlagged($this->messagesFromRequest($request), $request->boolean('flagged', true)));
    }

    public function move(Request $request): JsonResponse
    {
        $request->validate(['folder_id' => 'required|integer']);
        $target = $this->findFolder((int) $request->input('folder_id'));

        return $this->runAction(fn () => $this->actions->move($this->messagesFromRequest($request), $target));
    }

    public function trash(Request $request): JsonResponse
    {
        return $this->runAction(fn () => $this->actions->trash($this->messagesFromRequest($request)));
    }

    public function destroyForever(Request $request): JsonResponse
    {
        return $this->runAction(fn () => $this->actions->deleteForever($this->messagesFromRequest($request)));
    }

    public function archive(Request $request): JsonResponse
    {
        return $this->runAction(fn () => $this->actions->moveToType($this->messagesFromRequest($request), 'archive'));
    }

    /** Gereksize taşı; klasör tipi zaten junk ise gelen kutusuna geri al. */
    public function junk(Request $request): JsonResponse
    {
        $messages = $this->messagesFromRequest($request);
        $target = $request->boolean('not_junk') ? 'inbox' : 'junk';

        return $this->runAction(fn () => $this->actions->moveToType($messages, $target));
    }

    /**
     * Spam olarak işaretle / spam değil.
     *
     * Taşımadan ibaret olan junk()'tan farkı, göndericiyi de hatırlamasıdır:
     * kullanıcı bir iletiyi spam işaretlediğinde aynı adresten gelen sonraki
     * postaların da kendiliğinden gereksize düşmesini bekler. Bu yüzden burada
     * üç iş birden yapılır — ileti taşınır, gönderici engellenir, gelen
     * kutusunda bekleyen eski postaları da temizlenir.
     *
     * Engelleme sessiz değildir: arayüz işlemden önce "göndericiyi engelle" ve
     * "alan adının tamamını engelle" seçeneklerini sorar, kullanıcı yalnızca
     * taşımak istiyorsa block=false gönderir.
     *
     * Engel kapsamı acentenin tüm hesaplarıdır (account_id null): aynı spam
     * genelde her kutuya düşer, hesap hesap engellemek kullanıcıyı aynı işi
     * defalarca yapmaya zorlardı. Daralttmak isteyen kurallar penceresindeki
     * "Engellenen Göndericiler" sekmesinden hesap seçerek elle ekleyebilir.
     */
    public function spam(Request $request, MailSpamService $spam): JsonResponse
    {
        $messages = $this->messagesFromRequest($request);
        if ($messages->isEmpty()) {
            return response()->json(['ok' => false, 'message' => 'İşlem yapılacak ileti bulunamadı.'], 422);
        }

        $senders = $messages->pluck('from_email')->filter()->unique()->values();
        $notSpam = $request->boolean('not_spam');

        try {
            $moved = $this->actions->moveToType($messages, $notSpam ? 'inbox' : 'junk');

            if ($notSpam) {
                // Geri alma engeli de kaldırır; yoksa ileti bir sonraki
                // senkronda yeniden gereksize düşer ve kullanıcı bunu bir
                // hata sanar.
                $released = 0;
                foreach ($senders as $email) {
                    $released += $spam->unblockEmail($this->agencyId(), $email);
                }

                return response()->json([
                    'ok' => true,
                    'affected' => $moved,
                    'released' => $released,
                    'folders' => $this->folderCounts(),
                    'message' => $released > 0
                        ? 'Gelen kutusuna alındı; gönderici engeli kaldırıldı.'
                        : 'Gelen kutusuna alındı.',
                ]);
            }

            $blockRequested = $request->boolean('block', true);
            $kind = $request->boolean('block_domain')
                ? MailBlockedSender::KIND_DOMAIN
                : MailBlockedSender::KIND_EMAIL;

            $blockedCount = 0;
            $historyMoved = 0;
            $warning = null;

            if ($blockRequested) {
                foreach ($senders as $email) {
                    try {
                        $blocked = $spam->block($this->agencyId(), null, $email, $kind, (int) Auth::id());
                    } catch (\Throwable $e) {
                        // Engelleme reddedilse de (ör. kendi adresi) taşıma
                        // geçerlidir; işlemin tamamını geri almak yanlış olurdu
                        $warning = $e->getMessage();

                        continue;
                    }

                    if (! $blocked) {
                        continue;
                    }

                    $blockedCount++;

                    // Geçmişe uygulama isteğe bağlı: kullanıcı yalnızca bundan
                    // sonrasını engellemek isteyebilir.
                    if ($request->boolean('apply_existing', true)) {
                        $historyMoved += $spam->applyToInbox($blocked, $this->accountIds());
                    }
                }
            }

            $note = 'Gereksiz e-postaya taşındı.';
            if ($blockedCount > 0) {
                $note .= ' '.($kind === MailBlockedSender::KIND_DOMAIN ? 'Alan adı' : 'Gönderici').' engellendi.';
            }
            if ($historyMoved > 0) {
                $note .= ' Gelen kutusundaki '.$historyMoved.' eski ileti de taşındı.';
            }
            if ($warning !== null) {
                $note .= ' '.$warning;
            }

            return response()->json([
                'ok' => true,
                'affected' => $moved,
                'blocked' => $blockedCount,
                'history' => $historyMoved,
                'folders' => $this->folderCounts(),
                'message' => $note,
            ]);
        } catch (\Throwable $e) {
            return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
        } finally {
            $this->sync->closeAll();
        }
    }

    public function emptyFolder(Request $request): JsonResponse
    {
        $folder = $this->findFolder((int) $request->input('folder_id'));
        abort_unless(in_array($folder->type, ['trash', 'junk'], true), 422, 'Yalnızca Çöp ve Gereksiz klasörleri boşaltılabilir.');

        return $this->runAction(fn () => $this->actions->emptyFolder($folder));
    }

    /** İşlem sarmalayıcı: IMAP hatalarını okunur JSON'a çevirir. */
    private function runAction(callable $callback): JsonResponse
    {
        try {
            $result = $callback();
        } catch (\Throwable $e) {
            return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
        } finally {
            $this->sync->closeAll();
        }

        return response()->json([
            'ok' => true,
            'affected' => is_int($result) ? $result : null,
            'folders' => $this->folderCounts(),
        ]);
    }

    // ─────────────────────────────────────────── klasör yönetimi

    public function storeFolder(Request $request): JsonResponse
    {
        $request->validate([
            'account_id' => 'required|integer',
            'name' => 'required|string|max:100',
            'parent_id' => 'nullable|integer',
        ]);

        $account = $this->findAccount((int) $request->input('account_id'));
        $parent = $request->filled('parent_id') ? $this->findFolder((int) $request->input('parent_id')) : null;

        try {
            $folder = $this->actions->createFolder($account, $request->string('name'), $parent);
        } catch (\Throwable $e) {
            return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
        } finally {
            $this->sync->closeAll();
        }

        return response()->json(['ok' => true, 'folder' => ['id' => $folder->id, 'name' => $folder->name]]);
    }

    public function updateFolder(Request $request, int $id): JsonResponse
    {
        $request->validate(['name' => 'required|string|max:100']);
        $folder = $this->findFolder($id);

        try {
            $this->actions->renameFolder($folder, $request->string('name'));
        } catch (\Throwable $e) {
            return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
        } finally {
            $this->sync->closeAll();
        }

        return response()->json(['ok' => true]);
    }

    /**
     * Klasörü "Sık kullanılanlar" bölümüne sabitler / oradan çıkarır.
     *
     * Derin klasör ağaçlarında günlük kullanılan bir iki klasöre her seferinde
     * inmek yerine yukarıdan erişilir.
     */
    public function pinFolder(int $id): JsonResponse
    {
        $folder = $this->findFolder($id);
        $folder->forceFill(['is_pinned' => ! $folder->is_pinned])->save();

        return response()->json([
            'ok' => true,
            'pinned' => (bool) $folder->is_pinned,
            'message' => $folder->is_pinned ? 'Klasör sabitlendi.' : 'Sabitleme kaldırıldı.',
        ]);
    }

    public function destroyFolder(int $id): JsonResponse
    {
        $folder = $this->findFolder($id);

        try {
            $this->actions->deleteFolder($folder);
        } catch (\Throwable $e) {
            return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
        } finally {
            $this->sync->closeAll();
        }

        return response()->json(['ok' => true]);
    }

    // ─────────────────────────────────────────── gönderme

    public function send(Request $request): JsonResponse
    {
        $data = $this->validateCompose($request);
        $account = $this->findAccount((int) $request->input('account_id'));

        if ($error = $this->identityMismatch($account, $data['identity_id'])) {
            return $error;
        }

        $replyTo = null;
        if ($request->filled('reply_to_id')) {
            $replyTo = MailMessage::whereIn('account_id', $this->accountIds())->find((int) $request->input('reply_to_id'));
        }

        try {
            $this->sender->send($account, $data, $replyTo);
        } catch (\Throwable $e) {
            $this->sync->closeAll();

            return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
        }

        // Gönderim başarılı: otomatik kaydın taslağı artık kalıntıdır.
        // Ayrı try: taslak temizlenemese bile posta gitti, kullanıcıya
        // "gönderilemedi" demek yanlış olurdu.
        try {
            if ($draftId = trim((string) $request->input('draft_message_id'))) {
                $this->sender->discardDraft($account, $draftId);
            }
        } catch (\Throwable) {
            // Kalıntı taslak kullanıcı tarafından silinebilir
        } finally {
            $this->sync->closeAll();
        }

        return response()->json(['ok' => true, 'message' => 'E-posta gönderildi.', 'folders' => $this->folderCounts()]);
    }

    /**
     * Taslağı kaydeder — hem "Taslak" düğmesi hem de otomatik kayıt buradan geçer.
     *
     * Otomatik kayıt aynı taslağı yazma boyunca defalarca yazdığı için istemci
     * bir önceki sürümün Message-ID'sini geri gönderir; o sürüm sunucudan
     * kaldırılır ve Taslaklar klasöründe tek satır kalır.
     */
    public function saveDraft(Request $request): JsonResponse
    {
        $data = $this->validateCompose($request, requireRecipient: false);
        $account = $this->findAccount((int) $request->input('account_id'));

        if ($error = $this->identityMismatch($account, $data['identity_id'])) {
            return $error;
        }

        $replace = trim((string) $request->input('replace_message_id')) ?: null;

        try {
            $saved = $this->sender->saveDraft($account, $data, $replace);
        } catch (\Throwable $e) {
            return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
        } finally {
            $this->sync->closeAll();
        }

        return response()->json([
            'ok' => true,
            'message' => 'Taslak kaydedildi.',
            'draft' => $saved,
            'folders' => $this->folderCounts(),
        ]);
    }

    /**
     * Otomatik kaydedilmiş taslağı kaldırır.
     *
     * Kullanıcı iletiyi gönderdiğinde ya da yazma penceresini attığında kalıntı
     * taslak Taslaklar klasöründe durmamalı — gönderilmiş bir iletinin taslağı
     * kafa karıştırır.
     */
    public function discardDraft(Request $request): JsonResponse
    {
        $request->validate([
            'account_id' => 'required|integer',
            'message_id' => 'required|string|max:998',
        ]);

        $account = $this->findAccount((int) $request->input('account_id'));

        try {
            $this->sender->discardDraft($account, (string) $request->input('message_id'));
        } catch (\Throwable $e) {
            return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
        } finally {
            $this->sync->closeAll();
        }

        return response()->json(['ok' => true, 'folders' => $this->folderCounts()]);
    }

    /**
     * Seçilen gönderici profili gerçekten bu hesaba mı ait?
     *
     * Profil yalnızca görünen adı belirlese de, başka bir hesabın profiliyle
     * gönderim yapılabilmesi kendi adresinizden başka bir birimin adıyla
     * yazışmaya izin verirdi. Uyuşmazlıkta istek reddedilir.
     */
    private function identityMismatch(MailAccount $account, ?int $identityId): ?JsonResponse
    {
        if (! $identityId || $account->identities()->whereKey($identityId)->exists()) {
            return null;
        }

        return response()->json([
            'ok' => false,
            'message' => 'Seçilen gönderici profili bu hesaba ait değil.',
        ], 422);
    }

    /** Yazma formunu doğrular ve gönderim servisine uygun diziye çevirir. */
    private function validateCompose(Request $request, bool $requireRecipient = true): array
    {
        $request->validate([
            'account_id' => 'required|integer',
            'identity_id' => 'nullable|integer',
            // Otomatik kaydın zinciri: bir önceki taslağın Message-ID'si
            'replace_message_id' => 'nullable|string|max:998',
            'draft_message_id' => 'nullable|string|max:998',
            'to' => ($requireRecipient ? 'required' : 'nullable').'|string',
            'cc' => 'nullable|string',
            'bcc' => 'nullable|string',
            'subject' => 'nullable|string|max:900',
            'body_html' => 'nullable|string',
            'priority' => 'nullable|integer|in:1,3,5',
            'attachments.*' => 'file|max:25600',
        ]);

        $keep = collect();
        if ($request->filled('keep_attachment_ids')) {
            $ids = collect(explode(',', (string) $request->input('keep_attachment_ids')))
                ->map(fn ($v) => (int) trim($v))->filter();

            $keep = MailAttachment::whereIn('id', $ids)
                ->whereHas('message', fn ($q) => $q->whereIn('account_id', $this->accountIds()))
                ->get();
        }

        return [
            'identity_id' => $request->filled('identity_id') ? (int) $request->input('identity_id') : null,
            'to' => $this->parseAddressField($request->input('to')),
            'cc' => $this->parseAddressField($request->input('cc')),
            'bcc' => $this->parseAddressField($request->input('bcc')),
            'subject' => (string) $request->input('subject'),
            'body_html' => (string) $request->input('body_html'),
            'priority' => (int) $request->input('priority', 3),
            'in_reply_to' => $request->input('in_reply_to'),
            'references' => array_filter(explode(' ', (string) $request->input('references'))),
            'attachments' => $request->file('attachments', []),
            'keep_attachments' => $keep,
            'append_signature' => $request->boolean('append_signature'),
        ];
    }

    /** "Ad <a@b.com>, c@d.com" → [['name','email'], …] */
    private function parseAddressField(?string $value): array
    {
        return \App\Services\Mail\MimeParser::parseAddressList($value);
    }

    /** Alıcı alanı için otomatik tamamlama. */
    public function contacts(Request $request): JsonResponse
    {
        $term = trim((string) $request->input('q'));
        if (mb_strlen($term) < 2) {
            return response()->json([]);
        }

        // Adres defterinin yanı sıra panelin kendi kayıtları da aranır:
        // müşteri, tedarikçi, yetkili ve kaynaklara kart açmadan yazılabilsin.
        return response()->json(
            app(MailDirectory::class)->search($this->agencyId(), $term, null, 4)->take(10)->values()
        );
    }

    /**
     * Adres rehberi penceresi — kaynak sekmeleri ve arama.
     *
     * Yazma penceresindeki "Rehber" düğmesi buradan beslenir; kullanıcı
     * müşteri/tedarikçi/yetkili/kaynak listelerinden alıcı seçer.
     */
    public function directory(Request $request): JsonResponse
    {
        $request->validate([
            'kind' => 'nullable|string|in:'.implode(',', array_keys(MailDirectory::KINDS)),
            'q' => 'nullable|string|max:120',
        ]);

        $directory = app(MailDirectory::class);
        $agencyId = $this->agencyId();

        return response()->json([
            'kinds' => collect(MailDirectory::KINDS)
                ->map(fn ($spec, $key) => [
                    'key' => $key,
                    'label' => $spec['label'],
                    'icon' => $spec['icon'],
                    'color' => $spec['color'],
                ])->values(),
            'counts' => $directory->counts($agencyId),
            'items' => $directory->search(
                $agencyId,
                (string) $request->input('q', ''),
                $request->input('kind'),
                $request->filled('kind') ? 60 : 10,
            )->values(),
        ]);
    }

    // ─────────────────────────────────────────── ekler

    /**
     * Tarayıcıda gösterilmesine izin verilen ek türleri.
     *
     * Gönderenin belirlediği MIME'e güvenilemez: text/html bir ek panelin
     * kendi kaynağında (origin) çalışırsa oturumu ele geçirebilir. Bu yüzden
     * yalnızca aşağıdaki türler satır içi sunulur, kalan her şey indirilir.
     */
    private const INLINE_MIMES = [
        'image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp', 'image/bmp',
        'application/pdf', 'text/plain',
    ];

    public function showAttachment(int $id): StreamedResponse
    {
        $attachment = $this->findAttachment($id);
        $mime = mb_strtolower(trim((string) $attachment->mime));
        $mime = trim(explode(';', $mime)[0]);

        $inline = in_array($mime, self::INLINE_MIMES, true);

        return Storage::disk('local')->response($attachment->disk_path, $attachment->filename, [
            'Content-Type' => $inline ? $mime : 'application/octet-stream',
            'Content-Disposition' => $this->disposition($inline ? 'inline' : 'attachment', $attachment->filename),
            // Tarayıcı içeriğe bakıp tür tahmin etmesin
            'X-Content-Type-Options' => 'nosniff',
            'Content-Security-Policy' => "default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox",
        ]);
    }

    public function downloadAttachment(int $id): StreamedResponse
    {
        $attachment = $this->findAttachment($id);

        return Storage::disk('local')->response($attachment->disk_path, $attachment->filename, [
            'Content-Type' => 'application/octet-stream',
            'Content-Disposition' => $this->disposition('attachment', $attachment->filename),
            'X-Content-Type-Options' => 'nosniff',
        ]);
    }

    /**
     * RFC 6266 uyumlu Content-Disposition üretir.
     *
     * Dosya adı gönderenden gelir: tırnak ve satır sonu taşıyan bir ad elle
     * kaçırıldığında başlık enjeksiyonuna açık kalır; Symfony'nin kendi
     * üreticisi hem ASCII yedeğini hem UTF-8 biçimini doğru yazar.
     */
    private function disposition(string $type, string $filename): string
    {
        $safe = MimeParser::sanitizeFilename($filename) ?: 'ek';
        $ascii = preg_replace('/[^\x20-\x7E]/', '_', $safe) ?: 'ek';

        return HeaderUtils::makeDisposition($type, $safe, $ascii);
    }

    private function findAttachment(int $id): MailAttachment
    {
        $attachment = MailAttachment::whereHas('message', fn ($q) => $q->whereIn('account_id', $this->accountIds()))->find($id);

        abort_if(! $attachment || ! $attachment->disk_path, 404, 'Ek bulunamadı.');
        abort_unless(Storage::disk('local')->exists($attachment->disk_path), 404, 'Ek dosyası bulunamadı.');

        return $attachment;
    }

    /** Yazdırma görünümü. */
    public function print(int $id)
    {
        $message = MailMessage::whereIn('account_id', $this->accountIds())
            ->with(['attachments', 'account', 'folder'])
            ->findOrFail($id);

        if (! $message->body_fetched_at) {
            try {
                $message = $this->sync->ensureBody($message);
            } catch (\Throwable) {
                // Gövde alınamazsa mevcut veriyle yazdır
            } finally {
                $this->sync->closeAll();
            }
        }

        // Yazdırma sayfası gövdeyi doğrudan (çerçevesiz) çizer; temizleyiciden
        // kaçan bir betik kalırsa CSP'ye takılsın diye betik yalnızca bu
        // yanıtın nonce'ı ile çalışabilir.
        $nonce = rtrim(strtr(base64_encode(random_bytes(12)), '+/', '-_'), '=');

        return response()
            ->view('agency.mail.print', [
                'message' => $message,
                'nonce' => $nonce,
                'bodyHtml' => $message->body_html
                    ? MailHtmlSanitizer::clean($message->body_html, $message, true)
                    : MailHtmlSanitizer::fromText($message->body_text),
            ])
            ->header('Content-Security-Policy', implode('; ', [
                "default-src 'none'",
                "img-src 'self' data: https:",
                "style-src 'unsafe-inline'",
                "font-src 'self' data:",
                "script-src 'nonce-{$nonce}'",
                "form-action 'none'",
                "frame-src 'none'",
            ]));
    }
}
