<?php

namespace App\Services\Mail;

use App\Models\Mail\MailMessage;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
use ZipArchive;

/**
 * Posta dışa aktarma.
 *
 * Yedeklenen ham MIME metninden taşınabilir arşiv dosyaları üretir. Biçim
 * seçimi "hangi programda açılacak" sorusuna göre yapılır:
 *
 *   mbox     Thunderbird · Apple Mail · Gmail Takeout · Evolution (tek dosya)
 *   eml      Outlook · Windows Mail · Apple Mail (ileti başına dosya)
 *   maildir  Dovecot · Courier · qmail sunucuları (cur/new/tmp ağacı)
 *   pst_eml  Outlook'a içe aktarım için klasör yapılı EML paketi
 *   zip      EML + ekler + okunabilir dizin (genel amaçlı yedek)
 *   json     Yapısal tam veri (başka sisteme taşıma / programatik işleme)
 *   csv      Elektronik tablo — liste ve raporlama
 *   xlsx     Excel çalışma kitabı
 *   html     Tarayıcıda okunabilir tek dosyalık arşiv
 *   pdf      Basılabilir / imzalanabilir arşiv
 *
 * Üretilen dosya geçici dizine yazılır ve yolu döner; indirme sonrası silinir.
 */
class MailExportService
{
    /**
     * Kapsam sorgusunun üst sınırı — biçimden bağımsız güvenlik freni.
     *
     * Gerçek sınır biçime göre değişir (bkz. formats()[…]['max']): ham metni
     * doğrudan dosyaya akıtan MBOX yirmi bin iletiyi kaldırırken, tüm belgeyi
     * belleğe kurup sayfalayan PDF birkaç yüzü zor taşır.
     */
    public const MAX_MESSAGES = 20000;

    /** Geçici çıktıların diskte kalabileceği azami süre (saniye). */
    private const TEMP_TTL = 3600;

    public function __construct(private MailArchiveService $archives) {}

    /** Biçmin taşıyabileceği azami ileti sayısı. */
    public static function limitFor(string $format): int
    {
        return (int) (self::formats()[$format]['max'] ?? 1000);
    }

    /**
     * Desteklenen biçimler — arayüzdeki seçim menüsü de bunu okur.
     *
     * @return array<string, array{label:string,ext:string,mime:string,note:string,icon:string}>
     */
    public static function formats(): array
    {
        return [
            'mbox' => [
                'label' => 'MBOX',
                'ext' => 'mbox',
                'mime' => 'application/mbox',
                'note' => 'Thunderbird, Apple Mail, Gmail Takeout — tek dosya, en yaygın taşıma biçimi',
                'icon' => 'fa-box-archive',
                'max' => 20000,
            ],
            'eml' => [
                'label' => 'EML',
                'ext' => 'zip',
                'mime' => 'application/zip',
                'note' => 'Outlook ve Windows Mail — her ileti ayrı .eml dosyası',
                'icon' => 'fa-envelope',
                'max' => 5000,
            ],
            'maildir' => [
                'label' => 'Maildir',
                'ext' => 'zip',
                'mime' => 'application/zip',
                'note' => 'Dovecot, Courier, qmail — cur/new/tmp ağacı',
                'icon' => 'fa-folder-tree',
                'max' => 5000,
            ],
            'outlook' => [
                'label' => 'Outlook Paketi',
                'ext' => 'zip',
                'mime' => 'application/zip',
                'note' => 'Klasör yapısı korunmuş EML paketi — Outlook’a sürükleyip bırakın',
                'icon' => 'fa-window-restore',
                'max' => 5000,
            ],
            'zip' => [
                'label' => 'Tam Yedek (ZIP)',
                'ext' => 'zip',
                'mime' => 'application/zip',
                'note' => 'EML + ekler + dizin sayfası — arşivlik tam kopya',
                'icon' => 'fa-file-zipper',
                'max' => 1500,
            ],
            'json' => [
                'label' => 'JSON',
                'ext' => 'json',
                'mime' => 'application/json',
                'note' => 'Yapısal tam veri — başka sisteme taşıma ve programatik işleme',
                'icon' => 'fa-code',
                'max' => 3000,
            ],
            'csv' => [
                'label' => 'CSV',
                'ext' => 'csv',
                'mime' => 'text/csv',
                'note' => 'Elektronik tablo — liste, rapor ve filtreleme',
                'icon' => 'fa-file-csv',
                'max' => 20000,
            ],
            'xlsx' => [
                'label' => 'Excel',
                'ext' => 'xlsx',
                'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                'note' => 'Excel çalışma kitabı — biçimli liste',
                'icon' => 'fa-file-excel',
                'max' => 10000,
            ],
            'html' => [
                'label' => 'HTML Arşiv',
                'ext' => 'html',
                'mime' => 'text/html',
                'note' => 'Tarayıcıda okunabilir tek dosyalık arşiv',
                'icon' => 'fa-file-code',
                'max' => 1000,
            ],
            'pdf' => [
                'label' => 'PDF',
                'ext' => 'pdf',
                'mime' => 'application/pdf',
                'note' => 'Basılabilir / saklanabilir arşiv',
                'icon' => 'fa-file-pdf',
                'max' => 300,
            ],
        ];
    }

    public static function supports(string $format): bool
    {
        return array_key_exists($format, self::formats());
    }

    /**
     * Dışa aktarım dosyasını üretir.
     *
     * @return array{path:string,filename:string,mime:string,count:int}
     */
    public function export(Collection $messages, string $format, string $baseName = 'postalar'): array
    {
        if (! self::supports($format)) {
            throw new RuntimeException('Desteklenmeyen dışa aktarma biçimi: '.$format);
        }

        if ($messages->isEmpty()) {
            throw new RuntimeException('Dışa aktarılacak ileti bulunamadı.');
        }

        $meta = self::formats()[$format];

        if ($messages->count() > $meta['max']) {
            throw new RuntimeException(sprintf(
                '%s biçimi tek seferde en çok %s ileti taşıyabilir (%s ileti seçildi). '
                .'Tarih aralığını daraltın ya da MBOX gibi akışa yazılan bir biçim seçin.',
                $meta['label'],
                number_format($meta['max'], 0, ',', '.'),
                number_format($messages->count(), 0, ',', '.')
            ));
        }

        // Yarım kalmış önceki çıktılar diski şişirmesin
        $this->pruneTemp();

        $slug = Str::slug($baseName) ?: 'postalar';
        $filename = $slug.'-'.now()->format('Ymd-His').'.'.$meta['ext'];
        $path = $this->tempPath($filename);

        match ($format) {
            'mbox' => $this->writeMbox($messages, $path),
            'eml' => $this->writeEmlZip($messages, $path, false),
            'outlook' => $this->writeEmlZip($messages, $path, true),
            'maildir' => $this->writeMaildir($messages, $path),
            'zip' => $this->writeFullZip($messages, $path, $slug),
            'json' => $this->writeJson($messages, $path),
            'csv' => $this->writeCsv($messages, $path),
            'xlsx' => $this->writeXlsx($messages, $path),
            'html' => $this->writeHtml($messages, $path, $baseName),
            'pdf' => $this->writePdf($messages, $path, $baseName),
        };

        return [
            'path' => $path,
            'filename' => $filename,
            'mime' => $meta['mime'],
            'count' => $messages->count(),
        ];
    }

    // ─────────────────────────────────────────── ham biçimler

    /**
     * MBOX (mboxrd) — iletiler tek dosyada, her biri "From " ayracıyla.
     *
     * mboxrd kuralı: gövdede satır başındaki "From " ve ">From " dizileri
     * fazladan bir ">" ile kaçırılır; içe aktarımda geri alınır. Ayraç
     * satırındaki tarih ctime biçimindedir (yerel değil, İngilizce).
     */
    private function writeMbox(Collection $messages, string $path): void
    {
        $handle = $this->openFile($path);

        foreach ($messages as $message) {
            $raw = $this->archives->rawForExport($message);
            if ($raw === '') {
                continue;
            }

            $from = $message->from_email ?: 'bilinmeyen@localhost';
            $date = ($message->date_at ?: now())->format('D M d H:i:s Y');

            fwrite($handle, 'From '.$from.' '.$date."\n");
            fwrite($handle, preg_replace('/^(>*From )/m', '>$1', $this->normalizeEol($raw)));
            fwrite($handle, "\n\n");
        }

        fclose($handle);
    }

    /**
     * EML paketi. $foldered=true ise iletiler sunucudaki klasör ağacına göre
     * dizinlere ayrılır — Outlook'a sürükleyip bırakırken yapı korunur.
     */
    private function writeEmlZip(Collection $messages, string $path, bool $foldered): void
    {
        $zip = $this->openZip($path);
        $used = [];

        foreach ($messages as $message) {
            $raw = $this->archives->rawForExport($message);
            if ($raw === '') {
                continue;
            }

            $dir = $foldered ? $this->folderPath($message).'/' : '';
            $zip->addFromString($dir.$this->uniqueName($used, $dir, $this->emlName($message)), $this->normalizeEol($raw));
        }

        $zip->addFromString('OKUBENI.txt', $this->readme($messages, $foldered ? 'outlook' : 'eml'));
        $zip->close();
    }

    /**
     * Maildir — cur/ new/ tmp/ ağacı. Okunmuş iletiler cur/ altında ":2,S"
     * bayrağıyla, okunmamışlar new/ altında bayraksız durur.
     */
    private function writeMaildir(Collection $messages, string $path): void
    {
        $zip = $this->openZip($path);

        $zip->addEmptyDir('Maildir');
        foreach (['cur', 'new', 'tmp'] as $sub) {
            $zip->addEmptyDir('Maildir/'.$sub);
        }

        $seq = 0;
        foreach ($messages as $message) {
            $raw = $this->archives->rawForExport($message);
            if ($raw === '') {
                continue;
            }

            $seq++;
            $stamp = ($message->date_at ?: now())->getTimestamp();
            $unique = $stamp.'.'.$seq.'M'.$message->id.'.acente';

            // Maildir bayrakları: S okundu, F yıldızlı, R yanıtlanmış, D taslak
            $flags = ($message->is_seen ? 'S' : '').($message->is_flagged ? 'F' : '')
                .($message->is_answered ? 'R' : '').($message->is_draft ? 'D' : '');

            $name = $message->is_seen
                ? 'Maildir/cur/'.$unique.':2,'.($flags !== '' ? $flags : 'S')
                : 'Maildir/new/'.$unique;

            $zip->addFromString($name, $this->normalizeEol($raw));
        }

        $zip->addFromString('OKUBENI.txt', $this->readme($messages, 'maildir'));
        $zip->close();
    }

    /** Tam yedek: EML'ler + ekler + dizin sayfası + yapısal JSON. */
    private function writeFullZip(Collection $messages, string $path, string $slug): void
    {
        $zip = $this->openZip($path);
        $used = [];

        foreach ($messages as $message) {
            $raw = $this->archives->rawForExport($message);
            if ($raw !== '') {
                $zip->addFromString('iletiler/'.$this->uniqueName($used, 'iletiler/', $this->emlName($message)), $this->normalizeEol($raw));
            }

            foreach ($message->attachments as $attachment) {
                if (! $attachment->disk_path || ! Storage::disk('local')->exists($attachment->disk_path)) {
                    continue;
                }
                $dir = 'ekler/'.$message->id.'/';
                $zip->addFromString(
                    $dir.MimeParser::sanitizeFilename($attachment->filename),
                    Storage::disk('local')->get($attachment->disk_path)
                );
            }
        }

        // Yardımcı dosyalar önce diske yazılır: addFromString tüm içeriği
        // belleğe alır, bin iletilik bir dizin sayfası bunu kaldıramaz.
        $sidecars = [
            'dizin.html' => $this->tempPath($slug.'-dizin.tmp.html'),
            'veri.json' => $this->tempPath($slug.'-veri.tmp.json'),
            'liste.csv' => $this->tempPath($slug.'-liste.tmp.csv'),
        ];

        $this->writeHtml($messages, $sidecars['dizin.html'], 'Posta Arşivi');
        $this->writeJson($messages, $sidecars['veri.json']);
        $this->writeCsv($messages, $sidecars['liste.csv']);

        foreach ($sidecars as $name => $file) {
            $zip->addFile($file, $name);
        }

        $zip->addFromString('OKUBENI.txt', $this->readme($messages, 'zip'));
        $zip->close();

        // addFile arşiv kapanana kadar dosyayı okur; ancak bu noktada silinebilir
        foreach ($sidecars as $file) {
            @unlink($file);
        }
    }

    // ─────────────────────────────────────────── veri biçimleri

    /**
     * JSON çıktısı doğrudan dosyaya akıtılır.
     *
     * Gövde HTML'i ileti başına yüzlerce kilobayt olabildiği için tüm belgeyi
     * önce bellekte kurup sonra yazmak binlerce iletide bellek sınırını aşar;
     * iletiler tek tek kodlanıp yazılır.
     */
    private function writeJson(Collection $messages, string $path): void
    {
        $handle = $this->openFile($path);

        fwrite($handle, "{\n");
        fwrite($handle, '  "schema": "acente.mail.export/1",'."\n");
        fwrite($handle, '  "exported_at": '.json_encode(now()->toIso8601String()).",\n");
        fwrite($handle, '  "count": '.$messages->count().",\n");
        fwrite($handle, '  "messages": ['."\n");

        $first = true;
        foreach ($messages as $message) {
            fwrite($handle, ($first ? '' : ",\n").'    '.json_encode(
                $this->jsonEntry($message),
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
            ));
            $first = false;
        }

        fwrite($handle, "\n  ]\n}\n");
        fclose($handle);
    }

    /** @return array<string, mixed> */
    private function jsonEntry(MailMessage $m): array
    {
        return [
            'id' => $m->id,
            'account' => ['id' => $m->account_id, 'name' => $m->account?->name, 'email' => $m->account?->email],
            'folder' => ['name' => $m->folder?->name, 'type' => $m->folder?->type],
            'customer_id' => $m->customer_id,
            'direction' => $m->direction(),
            'message_id' => $m->message_id,
            'in_reply_to' => $m->in_reply_to,
            'references' => $m->references_json,
            'thread_key' => $m->thread_key,
            'subject' => $m->subject,
            'from' => ['name' => $m->from_name, 'email' => $m->from_email],
            'to' => $m->to_json,
            'cc' => $m->cc_json,
            'bcc' => $m->bcc_json,
            'date' => optional($m->date_at)->toIso8601String(),
            'snippet' => $m->snippet,
            'body_html' => $m->body_html,
            'body_text' => $m->body_text,
            'size' => $m->size,
            'flags' => [
                'seen' => (bool) $m->is_seen,
                'flagged' => (bool) $m->is_flagged,
                'answered' => (bool) $m->is_answered,
                'draft' => (bool) $m->is_draft,
            ],
            'priority' => $m->priority,
            'attachments' => $m->attachments->map(fn ($a) => [
                'filename' => $a->filename,
                'mime' => $a->mime,
                'size' => $a->size,
                'is_inline' => (bool) $a->is_inline,
            ])->values(),
        ];
    }

    /** Excel'in UTF-8'i doğru okuması için BOM ile başlar. */
    private function writeCsv(Collection $messages, string $path): void
    {
        $handle = $this->openFile($path);
        fwrite($handle, "\xEF\xBB\xBF");

        fputcsv($handle, $this->columns(), ';');
        foreach ($messages as $message) {
            fputcsv($handle, $this->row($message), ';');
        }

        fclose($handle);
    }

    private function writeXlsx(Collection $messages, string $path): void
    {
        $rows = $messages->map(fn (MailMessage $m) => $this->row($m))->values()->all();

        \Maatwebsite\Excel\Facades\Excel::store(
            new \App\Exports\MailMessagesExport($this->columns(), $rows),
            basename($path),
            'mail_exports',
            \Maatwebsite\Excel\Excel::XLSX
        );
    }

    /** @return string[] */
    private function columns(): array
    {
        return [
            'ID', 'Yön', 'Hesap', 'Klasör', 'Tarih', 'Kimden Ad', 'Kimden E-posta',
            'Kime', 'Bilgi', 'Konu', 'Özet', 'Ek Sayısı', 'Boyut (KB)',
            'Okundu', 'Yıldızlı', 'Yanıtlandı', 'Müşteri ID',
        ];
    }

    /** @return array<int, string|int> */
    private function row(MailMessage $m): array
    {
        return [
            $m->id,
            $m->direction() === 'outbound' ? 'Giden' : 'Gelen',
            (string) $m->account?->name,
            (string) $m->folder?->name,
            optional($m->date_at)->format('d.m.Y H:i'),
            (string) $m->from_name,
            (string) $m->from_email,
            collect($m->to_json ?: [])->pluck('email')->implode(', '),
            collect($m->cc_json ?: [])->pluck('email')->implode(', '),
            (string) $m->subject,
            (string) $m->snippet,
            $m->attachments->where('is_inline', false)->count(),
            round(((int) $m->size) / 1024, 1),
            $m->is_seen ? 'Evet' : 'Hayır',
            $m->is_flagged ? 'Evet' : 'Hayır',
            $m->is_answered ? 'Evet' : 'Hayır',
            (string) ($m->customer_id ?: ''),
        ];
    }

    // ─────────────────────────────────────────── okunabilir biçimler

    /**
     * HTML arşiv — parça parça dosyaya yazılır.
     *
     * İleti gövdeleri tek tek üretilip akıtıldığı için bin iletilik bir arşiv
     * de belleğe sığar; tümünü birleştirip tek dizge kurmak sığmazdı.
     */
    private function writeHtml(Collection $messages, string $path, string $title): void
    {
        $handle = $this->openFile($path);

        fwrite($handle, $this->htmlHead($messages, $title, true, false));
        foreach ($messages as $message) {
            fwrite($handle, $this->htmlItem($message, true));
        }
        fwrite($handle, $this->htmlFoot());

        fclose($handle);
    }

    private function writePdf(Collection $messages, string $path, string $title): void
    {
        $document = $this->htmlHead($messages, $title, false, true)
            .$messages->map(fn (MailMessage $m) => $this->htmlItem($m, false))->implode('')
            .$this->htmlFoot();

        $pdf = \Barryvdh\DomPDF\Facade\Pdf::loadHTML($document)
            ->setPaper('a4')
            ->setOptions(['isRemoteEnabled' => false, 'isHtml5ParserEnabled' => true, 'defaultFont' => 'DejaVu Sans']);

        file_put_contents($path, $pdf->output());
    }

    /**
     * Okunabilir arşivin açılış bloğu: belge kabuğu, biçem ve başlık şeridi.
     *
     * @param  bool  $interactive  İletiler katlanır (HTML çıktısı için)
     * @param  bool  $forPdf       PDF için sadeleştirilmiş, sayfa sonu duyarlı düzen
     */
    private function htmlHead(Collection $messages, string $title, bool $interactive, bool $forPdf): string
    {
        $e = fn ($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
        $range = $messages->min('date_at').' — '.$messages->max('date_at');

        // Yazı tipi PDF'te gömülü DejaVu Sans; HTML'de sistem yığını
        $font = $forPdf ? "'DejaVu Sans', sans-serif" : "-apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif";

        // Arşiv dosyası tarayıcıda açılır ve ileti gövdesi taşır. Temizleyiciden
        // kaçan bir betik kalmışsa çalışamasın diye belge tamamen betiksizdir
        // ve CSP ile mühürlenir; katlama <details> ile yapılır.
        $csp = $forPdf ? '' : '<meta http-equiv="Content-Security-Policy" content="'
            ."default-src 'none'; img-src * data:; style-src 'unsafe-inline'; "
            ."script-src 'none'; object-src 'none'; form-action 'none'\">";

        return '<!doctype html><html lang="tr"><head><meta charset="utf-8">'.$csp
            .'<title>'.$e($title).'</title><style>'
            .'*{box-sizing:border-box}'
            .'body{margin:0;padding:24px;background:#f1f5f9;color:#0f172a;font:13px/1.6 '.$font.'}'
            .'.wrap{max-width:900px;margin:0 auto}'
            .'.top{background:#fff;border:1px solid #e2e8f0;border-radius:14px;padding:18px 20px;margin-bottom:16px}'
            .'.top h1{margin:0 0 4px;font-size:19px;letter-spacing:-.02em}'
            .'.top p{margin:0;color:#64748b;font-size:12px}'
            .'.m{background:#fff;border:1px solid #e2e8f0;border-radius:14px;margin-bottom:12px;overflow:hidden;'
            .($forPdf ? 'page-break-inside:avoid;' : '').'}'
            .'.m.out{border-left:3px solid #2563eb}'
            .'.m:not(.out){border-left:3px solid #f59e0b}'
            .'header{padding:13px 16px;border-bottom:1px solid #f1f5f9'.($interactive ? ';cursor:pointer' : '').'}'
            .'.dir{display:inline-block;font-size:9px;font-weight:800;letter-spacing:.08em;color:#64748b;'
            .'background:#f1f5f9;border-radius:999px;padding:2px 8px;margin-bottom:6px}'
            .'header h2{margin:0 0 6px;font-size:15px;letter-spacing:-.01em}'
            .'.meta{display:flex;flex-wrap:wrap;gap:4px 16px;font-size:11.5px;color:#64748b}'
            .'.meta b{color:#94a3b8;font-weight:700;font-size:10px;letter-spacing:.05em;text-transform:uppercase;margin-right:4px}'
            .'.body{padding:14px 16px;word-break:break-word}'
            .'.body img{max-width:100%;height:auto}'
            .'.body table{max-width:100%}'
            .'.atts{padding:10px 16px;border-top:1px solid #f1f5f9;display:flex;flex-wrap:wrap;gap:6px}'
            .'.atts span{font-size:11px;color:#475569;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:3px 9px}'
            .($interactive
                ? 'summary{list-style:none;display:block;position:relative;padding-right:34px}'
                    .'summary::-webkit-details-marker{display:none}'
                    .'summary::after{content:"\\25B8";position:absolute;right:16px;top:14px;color:#94a3b8;font-size:12px}'
                    .'details[open]>summary::after{content:"\\25BE"}'
                : '')
            .'</style></head><body><div class="wrap">'
            .'<div class="top"><h1>'.$e($title).'</h1>'
            .'<p>'.$messages->count().' ileti · '.$e($range).' · Dışa aktarma: '.now()->format('d.m.Y H:i').'</p></div>';
    }

    /** Tek iletinin okunabilir arşivdeki bloğu. */
    private function htmlItem(MailMessage $message, bool $interactive): string
    {
        $e = fn ($v) => htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');

        $body = $message->body_html
            ? MailHtmlSanitizer::clean($message->body_html, $message, false)
            : MailHtmlSanitizer::fromText($message->body_text ?: $message->snippet);

        $to = collect($message->to_json ?: [])->pluck('email')->implode(', ');
        $atts = $message->attachments->where('is_inline', false);
        $outbound = $message->direction() === 'outbound';

        // Katlanır çıktıda <details>/<summary>; PDF'te dompdf bunları
        // tanımadığı için düz <article>/<header> kullanılır.
        $tag = $interactive ? 'details' : 'article';
        $head = $interactive ? 'summary' : 'header';

        return '<'.$tag.' class="m'.($outbound ? ' out' : '').'">'
            .'<'.$head.'>'
            .'<span class="dir">'.($outbound ? 'GİDEN' : 'GELEN').'</span>'
            .'<h2>'.$e($message->subject ?: '(konu yok)').'</h2>'
            .'<div class="meta">'
            .'<span><b>Kimden</b> '.$e(trim(($message->from_name ?: '').' <'.$message->from_email.'>')).'</span>'
            .($to ? '<span><b>Kime</b> '.$e($to).'</span>' : '')
            .'<span><b>Tarih</b> '.$e(optional($message->date_at)->format('d.m.Y H:i')).'</span>'
            .($atts->count() ? '<span><b>Ek</b> '.$atts->count().' dosya</span>' : '')
            .'</div></'.$head.'>'
            .'<div class="body">'.$body.'</div>'
            .($atts->count()
                ? '<div class="atts">'.$atts->map(fn ($a) => '<span>'.$e($a->filename).' · '.$e($a->humanSize()).'</span>')->implode('').'</div>'
                : '')
            .'</'.$tag.'>';
    }

    private function htmlFoot(): string
    {
        return '</div></body></html>';
    }

    // ─────────────────────────────────────────── yardımcılar

    /** Arşivin nasıl kullanılacağını anlatan not — ZIP paketlerine eklenir. */
    private function readme(Collection $messages, string $format): string
    {
        $howto = match ($format) {
            'maildir' => "Maildir/ klasörünü posta sunucunuzun kullanıcı dizinine kopyalayın\n"
                ."(Dovecot, Courier ve qmail bu yapıyı doğrudan okur).",
            'outlook' => "Klasörleri açın ve .eml dosyalarını Outlook'ta hedef klasöre sürükleyip bırakın.\n"
                ."Windows Mail ve Apple Mail de aynı dosyaları açar.",
            'eml' => "Her .eml dosyası tek bir iletidir. Outlook, Windows Mail, Apple Mail ve\n"
                ."Thunderbird bu dosyaları çift tıklayarak açar.",
            default => "iletiler/  → her ileti ayrı .eml dosyası (her istemcide açılır)\n"
                ."ekler/     → ileti kimliğine göre ayrılmış ek dosyaları\n"
                ."dizin.html → tarayıcıda okunabilir liste\n"
                ."veri.json  → yapısal tam veri\n"
                ."liste.csv  → elektronik tablo",
        };

        return "POSTA ARŞİVİ\n"
            ."==================================================\n"
            .'Dışa aktarma  : '.now()->format('d.m.Y H:i')."\n"
            .'İleti sayısı  : '.$messages->count()."\n"
            .'Tarih aralığı : '.$messages->min('date_at').' — '.$messages->max('date_at')."\n\n"
            ."NASIL KULLANILIR\n"
            ."--------------------------------------------------\n"
            .$howto."\n\n"
            ."NOT\n"
            ."--------------------------------------------------\n"
            ."X-Archive-Rebuilt başlığı taşıyan iletilerin ham hâli sunucudan\n"
            ."alınamamış, saklanan alanlardan yeniden kurulmuştur. İçerik ve\n"
            ."ekler yerindedir; özgün başlıkların tamamını taşımaz.\n";
    }

    private function emlName(MailMessage $message): string
    {
        $date = optional($message->date_at)->format('Ymd-His') ?: 'tarihsiz';
        $subject = Str::slug(mb_substr((string) ($message->subject ?: 'konusuz'), 0, 60)) ?: 'konusuz';

        return $date.'-'.$subject.'-'.$message->id.'.eml';
    }

    /** Sunucudaki klasör adından güvenli bir dizin yolu üretir. */
    private function folderPath(MailMessage $message): string
    {
        $name = (string) ($message->folder?->name ?: 'Diger');
        $safe = preg_replace('#[\\\\/:*?"<>|]+#u', '-', $name) ?? $name;

        return trim(mb_substr($safe, 0, 60)) ?: 'Diger';
    }

    /** Aynı ada sahip dosyalar birbirini ezmesin. */
    private function uniqueName(array &$used, string $dir, string $name): string
    {
        $key = $dir.mb_strtolower($name);
        if (! isset($used[$key])) {
            $used[$key] = 1;

            return $name;
        }

        $used[$key]++;

        return preg_replace('/\.eml$/', '', $name).'-'.$used[$key].'.eml';
    }

    /** MIME satır sonları CRLF olmalı; karışık gelen metinler düzeltilir. */
    private function normalizeEol(string $raw): string
    {
        return preg_replace("/\r\n|\r|\n/", "\r\n", $raw) ?? $raw;
    }

    private function openZip(string $path): ZipArchive
    {
        $zip = new ZipArchive;

        if ($zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            throw new RuntimeException('Arşiv dosyası oluşturulamadı.');
        }

        return $zip;
    }

    /** Yazma için dosya açar; açılamazsa sessizce boş çıktı üretmez. */
    private function openFile(string $path)
    {
        $handle = @fopen($path, 'wb');

        if ($handle === false) {
            throw new RuntimeException('Dışa aktarma dosyası oluşturulamadı: '.basename($path));
        }

        return $handle;
    }

    /** Geçici çıktı dizini — indirme sonrası temizlenir. */
    private function tempPath(string $filename): string
    {
        return $this->tempDir().DIRECTORY_SEPARATOR.$filename;
    }

    private function tempDir(): string
    {
        $dir = storage_path('app/mail-exports');

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

        return $dir;
    }

    /**
     * Geride kalan geçici çıktıları siler.
     *
     * Normal akışta dosya indirme bittiğinde silinir; ancak kullanıcı indirmeyi
     * iptal ederse ya da üretim ortasında hata alınırsa dosya diskte kalır.
     * Bu temizlik hem her dışa aktarımın başında hem `mail:prune-exports`
     * görevinde çalışır.
     *
     * @return int Silinen dosya sayısı
     */
    public function pruneTemp(?int $ttl = null): int
    {
        $ttl = $ttl ?? self::TEMP_TTL;
        $threshold = time() - max(60, $ttl);
        $removed = 0;

        foreach (glob($this->tempDir().DIRECTORY_SEPARATOR.'*') ?: [] as $file) {
            if (! is_file($file) || filemtime($file) > $threshold) {
                continue;
            }

            if (@unlink($file)) {
                $removed++;
            }
        }

        return $removed;
    }
}
