<?php

namespace App\Services\Mail;

use App\Models\Mail\MailAccount;
use App\Models\Mail\MailArchive;
use App\Models\Mail\MailMessage;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;

/**
 * Posta yedekleme.
 *
 * Sunucudaki ham MIME metnini indirip gzip'leyerek veritabanına yazar. Ham
 * metin kaybı olmayan tek kaynaktır: EML, MBOX, Maildir gibi taşınabilir
 * biçimlerin hepsi ondan üretilir, dolayısıyla dışa aktarılan arşiv başka
 * posta istemcilerinde birebir açılır.
 *
 * Sunucuya ulaşılamayan ya da yerelde doğmuş (gönderilenler kopyası oluşmamış)
 * iletiler için ham metin saklanan alanlardan yeniden kurulur — biçim yine
 * geçerli MIME'dir, yalnızca özgün başlıkların tamamını taşımaz.
 */
class MailArchiveService
{
    /** Veritabanına yazılacak azami sıkıştırılmış boyut; üstü diske gider. */
    private const MAX_DB_BYTES = 8 * 1024 * 1024;

    public function __construct(private MailSyncService $sync) {}

    // ─────────────────────────────────────────── yedekleme

    /**
     * Tek iletiyi yedekler: gövde + ekler veritabanına indirilir, ham MIME
     * sıkıştırılıp saklanır.
     */
    public function archive(MailMessage $message, bool $force = false): ?MailArchive
    {
        $existing = MailArchive::where('message_id', $message->id)->first();
        if ($existing && ! $force && ! $existing->is_rebuilt) {
            return $existing;
        }

        // Gövde ve ekler de yedeğin parçası — okunmamış iletilerde henüz inmemiş olur
        $rebuilt = false;
        $raw = $this->fetchRaw($message);

        if ($raw === null) {
            $raw = $this->rebuildRaw($message);
            $rebuilt = true;
        }

        if ($raw === null || $raw === '') {
            return $existing;
        }

        return $this->store($message, $raw, $rebuilt, $existing);
    }

    /**
     * Bir dizi iletiyi yedekler.
     *
     * @return array{archived:int,skipped:int,failed:int,bytes:int}
     */
    public function archiveMany(iterable $messages, bool $force = false): array
    {
        $stats = ['archived' => 0, 'skipped' => 0, 'failed' => 0, 'bytes' => 0];

        foreach ($messages as $message) {
            try {
                $before = MailArchive::where('message_id', $message->id)->exists();
                $archive = $this->archive($message, $force);

                if (! $archive) {
                    $stats['failed']++;

                    continue;
                }

                if ($before && ! $force) {
                    $stats['skipped']++;
                } else {
                    $stats['archived']++;
                    $stats['bytes'] += (int) $archive->stored_size;
                }
            } catch (\Throwable $e) {
                $stats['failed']++;
                Log::warning('Posta yedeklenemedi', ['message' => $message->id, 'error' => $e->getMessage()]);
            }
        }

        return $stats;
    }

    /** Hesabın henüz yedeklenmemiş iletileri. */
    public function pendingQuery(MailAccount $account)
    {
        return MailMessage::where('account_id', $account->id)
            ->where('is_deleted', false)
            ->whereNotExists(fn ($q) => $q
                ->selectRaw(1)
                ->from('mail_archives')
                ->whereColumn('mail_archives.message_id', 'mail_messages.id')
                ->where('mail_archives.is_rebuilt', false))
            ->orderBy('id');
    }

    // ─────────────────────────────────────────── ham metin

    /** Yedekten (yoksa sunucudan, o da yoksa yeniden kurarak) ham MIME döner. */
    public function raw(MailMessage $message): ?string
    {
        $archive = MailArchive::where('message_id', $message->id)->first();

        if ($archive && ($raw = $archive->raw())) {
            return $raw;
        }

        return $this->fetchRaw($message) ?? $this->rebuildRaw($message);
    }

    /**
     * Dışa aktarma için ham metin — sunucuya gitmez.
     *
     * Yedeklenmemiş iletiler saklanan alanlardan kurulur; böylece dışa aktarma
     * IMAP bağlantısı olmadan da her zaman çalışır.
     */
    public function rawForExport(MailMessage $message): string
    {
        $archive = $message->relationLoaded('archive')
            ? $message->archive
            : MailArchive::where('message_id', $message->id)->first();

        if ($archive && ($raw = $archive->raw())) {
            return $raw;
        }

        return (string) $this->rebuildRaw($message);
    }

    /** Sunucudan ham metni indirir (bağlantı yoksa null). */
    private function fetchRaw(MailMessage $message): ?string
    {
        if (! $message->uid || ! $message->account || ! $message->folder) {
            return null;
        }

        try {
            $client = $this->sync->client($message->account);
            $client->select($message->folder->path, true);   // EXAMINE: okundu bayrağı bozulmasın

            $raw = $client->fetchRaw((int) $message->uid);

            // Aynı indirmeden gövde ve ekler de çıkarılır — yedek yalnızca ham
            // metinden ibaret değil, ileti panelde de tam görünür olmalı
            if ($raw !== null && ! $message->body_fetched_at) {
                $this->sync->storeParsedBody($message, $raw);
                $message->refresh();
            }

            return $raw;
        } catch (\Throwable $e) {
            Log::info('Ham posta indirilemedi, yeniden kurulacak', [
                'message' => $message->id, 'error' => $e->getMessage(),
            ]);

            return null;
        }
    }

    /**
     * Saklanan alanlardan geçerli bir MIME iletisi kurar.
     *
     * Özgün başlıkların tamamı korunmaz; ama konu, taraflar, tarih, gövde ve
     * ekler yerinde olduğu için dışa aktarılan dosya her istemcide açılır.
     */
    public function rebuildRaw(MailMessage $message): ?string
    {
        try {
            $email = new Email;

            $from = trim((string) $message->from_email);
            if ($from !== '' && filter_var($from, FILTER_VALIDATE_EMAIL)) {
                $email->from(new Address($from, (string) ($message->from_name ?: '')));
            } else {
                $email->from(new Address('bilinmeyen@localhost', (string) ($message->from_name ?: 'Bilinmiyor')));
            }

            foreach ([['to_json', 'addTo'], ['cc_json', 'addCc'], ['bcc_json', 'addBcc']] as [$field, $method]) {
                foreach ((array) ($message->{$field} ?: []) as $item) {
                    $address = trim((string) ($item['email'] ?? ''));
                    if ($address === '' || ! filter_var($address, FILTER_VALIDATE_EMAIL)) {
                        continue;
                    }
                    $email->{$method}(new Address($address, (string) ($item['name'] ?? '')));
                }
            }

            $email->subject((string) ($message->subject ?: '(konu yok)'));
            $email->date($message->date_at ? $message->date_at->toDateTimeImmutable() : now()->toDateTimeImmutable());

            $html = (string) $message->body_html;
            $text = (string) $message->body_text;

            if ($html !== '') {
                $email->html($html);
            }
            if ($text !== '') {
                $email->text($text);
            } elseif ($html === '') {
                $email->text((string) $message->snippet);
            }

            foreach ($message->attachments as $attachment) {
                if (! $attachment->disk_path || ! Storage::disk('local')->exists($attachment->disk_path)) {
                    continue;
                }
                $email->attach(
                    (string) Storage::disk('local')->get($attachment->disk_path),
                    $attachment->filename,
                    $attachment->mime ?: 'application/octet-stream'
                );
            }

            $headers = $email->getHeaders();
            if ($message->message_id && ! $headers->has('Message-ID')) {
                $headers->addIdHeader('Message-ID', trim((string) $message->message_id, '<> '));
            }
            if ($message->in_reply_to) {
                $headers->addTextHeader('In-Reply-To', '<'.trim((string) $message->in_reply_to, '<> ').'>');
            }
            if ($refs = (array) ($message->references_json ?: [])) {
                $headers->addTextHeader('References', collect($refs)->map(fn ($r) => '<'.trim((string) $r, '<> ').'>')->implode(' '));
            }
            // Yeniden kurulduğu dosyadan da anlaşılsın
            $headers->addTextHeader('X-Archive-Rebuilt', 'yes');

            return $email->toString();
        } catch (\Throwable $e) {
            Log::warning('Ham posta yeniden kurulamadı', ['message' => $message->id, 'error' => $e->getMessage()]);

            return null;
        }
    }

    // ─────────────────────────────────────────── saklama

    private function store(MailMessage $message, string $raw, bool $rebuilt, ?MailArchive $existing): MailArchive
    {
        $packed = gzencode($raw, 6);
        $compression = 'gzip';

        if ($packed === false) {
            $packed = $raw;
            $compression = 'none';
        }

        $attributes = [
            'message_id' => $message->id,
            'account_id' => $message->account_id,
            'agency_id' => (int) ($message->account?->agency_id ?? 0),
            'raw_size' => strlen($raw),
            'stored_size' => strlen($packed),
            'checksum' => hash('sha256', $raw),
            'compression' => $compression,
            'is_rebuilt' => $rebuilt,
            'archived_at' => now(),
            'raw_gz' => null,
            'raw_path' => null,
        ];

        // Büyük iletiler satıra sığmaz: diske yaz, satırda yalnızca yolu tut
        if (strlen($packed) > self::MAX_DB_BYTES) {
            $path = "mail/archive/{$message->account_id}/{$message->id}.eml.gz";
            Storage::disk('local')->put($path, $packed);
            $attributes['raw_path'] = $path;
        } else {
            $attributes['raw_gz'] = $packed;
            // Önceki büyük sürümden kalan dosya varsa temizlensin
            if ($existing?->raw_path && Storage::disk('local')->exists($existing->raw_path)) {
                Storage::disk('local')->delete($existing->raw_path);
            }
        }

        if ($existing) {
            $existing->forceFill($attributes)->save();

            return $existing;
        }

        return MailArchive::create($attributes);
    }

    // ─────────────────────────────────────────── özet

    /**
     * Acente için yedek durumu — arayüzdeki "yedeklendi / bekliyor" özeti.
     *
     * @return array{messages:int,archived:int,pending:int,bytes:int,raw_bytes:int,last_at:?string}
     */
    public function summary(array $accountIds): array
    {
        if (! $accountIds) {
            return ['messages' => 0, 'archived' => 0, 'pending' => 0, 'bytes' => 0, 'raw_bytes' => 0, 'last_at' => null];
        }

        $messages = MailMessage::whereIn('account_id', $accountIds)->where('is_deleted', false)->count();

        $row = MailArchive::whereIn('account_id', $accountIds)
            ->selectRaw('COUNT(*) as n, COALESCE(SUM(stored_size),0) as stored, COALESCE(SUM(raw_size),0) as raw, MAX(archived_at) as last_at')
            ->first();

        $archived = (int) ($row->n ?? 0);

        return [
            'messages' => $messages,
            'archived' => $archived,
            'pending' => max(0, $messages - $archived),
            'bytes' => (int) ($row->stored ?? 0),
            'raw_bytes' => (int) ($row->raw ?? 0),
            'last_at' => $row->last_at ? (string) $row->last_at : null,
        ];
    }

    /** Yedeklenecek iletileri toplu hazırlar (dışa aktarmadan hemen önce). */
    public function ensureArchived(Collection $messages): void
    {
        $missing = $messages->filter(fn (MailMessage $m) => ! MailArchive::where('message_id', $m->id)->exists());

        if ($missing->isNotEmpty()) {
            $this->archiveMany($missing);
        }
    }

    public function closeConnections(): void
    {
        $this->sync->closeAll();
    }
}
