<?php

namespace App\Services\Mail;

use Carbon\Carbon;

/**
 * RFC 822 / MIME çözümleyici.
 *
 * IMAP'ten gelen ham mesajı (veya yalnızca başlık bloğunu) başlık, gövde ve
 * ek listesine ayırır. ext-mailparse / ext-imap gerektirmez.
 */
class MimeParser
{
    /**
     * Yalnızca başlık bloğunu çözer (liste görünümü için yeterli).
     *
     * @return array{headers:array<string,string>,subject:string,from:array{name:string,email:string},to:array,cc:array,bcc:array,reply_to:array,message_id:?string,in_reply_to:?string,references:array,date:?Carbon,priority:int,has_attachments:bool}
     */
    public static function parseHeaders(string $rawHeaders): array
    {
        $headers = self::headerMap($rawHeaders);

        $contentType = $headers['content-type'] ?? '';
        $disposition = $headers['content-disposition'] ?? '';

        return [
            'headers' => $headers,
            'subject' => self::decodeWords($headers['subject'] ?? ''),
            'from' => self::firstAddress($headers['from'] ?? ''),
            'to' => self::parseAddressList($headers['to'] ?? ''),
            'cc' => self::parseAddressList($headers['cc'] ?? ''),
            'bcc' => self::parseAddressList($headers['bcc'] ?? ''),
            'reply_to' => self::parseAddressList($headers['reply-to'] ?? ''),
            'message_id' => self::cleanId($headers['message-id'] ?? null),
            'in_reply_to' => self::cleanId($headers['in-reply-to'] ?? null),
            'references' => self::parseReferences($headers['references'] ?? ''),
            'date' => self::parseDate($headers['date'] ?? null),
            'priority' => self::parsePriority($headers),
            // Kesin bilgi gövde çekilince netleşir; listede ataç ikonu için yaklaşık kestirim
            'has_attachments' => (bool) preg_match('/multipart\/(mixed|related)/i', $contentType)
                || str_contains(mb_strtolower($disposition), 'attachment'),
        ];
    }

    /**
     * Tam mesajı çözer: başlıklar + metin/HTML gövde + ekler.
     *
     * @return array Başlık çıktısına ek olarak: html, text, attachments
     */
    public static function parse(string $raw): array
    {
        [$rawHeaders, $body] = self::splitMessage($raw);
        $result = self::parseHeaders($rawHeaders);
        $headers = $result['headers'];

        $parts = [];
        self::walkPart(
            $headers['content-type'] ?? 'text/plain',
            $headers['content-transfer-encoding'] ?? '7bit',
            $headers['content-disposition'] ?? '',
            $headers['content-id'] ?? '',
            $body,
            '1',
            $parts
        );

        $html = null;
        $text = null;
        $attachments = [];

        foreach ($parts as $part) {
            if ($part['is_attachment']) {
                $attachments[] = [
                    'filename' => $part['filename'] ?: 'ek-'.$part['part_id'],
                    'mime' => $part['mime'],
                    'size' => strlen($part['content']),
                    'content' => $part['content'],
                    'content_id' => $part['content_id'],
                    'is_inline' => $part['is_inline'],
                    'part_id' => $part['part_id'],
                ];

                continue;
            }

            if ($part['mime'] === 'text/html' && $html === null) {
                $html = $part['content'];
            } elseif ($part['mime'] === 'text/plain' && $text === null) {
                $text = $part['content'];
            }
        }

        $result['html'] = $html;
        $result['text'] = $text;
        $result['attachments'] = $attachments;
        $result['has_attachments'] = (bool) array_filter($attachments, fn ($a) => ! $a['is_inline']);

        return $result;
    }

    // ─────────────────────────────────────────── başlıklar

    /** Ham mesajı [başlık bloğu, gövde] olarak ikiye ayırır. */
    public static function splitMessage(string $raw): array
    {
        $raw = str_replace("\r\n", "\n", $raw);
        $pos = strpos($raw, "\n\n");

        if ($pos === false) {
            return [$raw, ''];
        }

        return [substr($raw, 0, $pos), substr($raw, $pos + 2)];
    }

    /**
     * Katlanmış başlıkları açar, küçük harfe indirgenmiş anahtar → değer haritası döner.
     * Aynı başlık birden fazlaysa değerler "\n" ile birleştirilir (Received gibi).
     */
    public static function headerMap(string $rawHeaders): array
    {
        $rawHeaders = str_replace("\r\n", "\n", $rawHeaders);
        $unfolded = preg_replace('/\n[ \t]+/', ' ', $rawHeaders) ?? $rawHeaders;

        $map = [];
        foreach (explode("\n", $unfolded) as $line) {
            if (! str_contains($line, ':')) {
                continue;
            }
            [$key, $value] = explode(':', $line, 2);
            $key = strtolower(trim($key));
            $value = trim($value);

            $map[$key] = isset($map[$key]) ? $map[$key]."\n".$value : $value;
        }

        return $map;
    }

    /** =?UTF-8?B?...?= biçimli kodlu kelimeleri çözer. */
    public static function decodeWords(string $value): string
    {
        if ($value === '') {
            return '';
        }

        if (! str_contains($value, '=?')) {
            return self::toUtf8($value, self::guessCharset($value));
        }

        // Bitişik kodlu kelimeler arasındaki boşluk RFC gereği yok sayılır
        $value = (string) preg_replace('/\?=[ \t]+=\?/', '?==?', $value);

        $decoded = preg_replace_callback(
            '/=\?([^?]+)\?([BbQq])\?([^?]*)\?=/',
            function ($m) {
                $charset = $m[1];
                $text = strtoupper($m[2]) === 'B'
                    ? (base64_decode($m[3], false) ?: '')
                    : quoted_printable_decode(str_replace('_', ' ', $m[3]));

                return self::toUtf8($text, $charset);
            },
            $value
        );

        return trim((string) $decoded);
    }

    /** Bilinmeyen/eksik karakter kümesini UTF-8'e çevirir. */
    public static function toUtf8(string $text, ?string $charset): string
    {
        $charset = strtoupper(trim((string) $charset));
        $charset = preg_replace('/[^A-Z0-9\-_]/', '', $charset) ?: 'UTF-8';

        $aliases = [
            '' => 'UTF-8', 'UTF8' => 'UTF-8', 'USASCII' => 'ASCII', 'US-ASCII' => 'ASCII',
            'ISO88599' => 'ISO-8859-9', 'ISO-8859-9' => 'ISO-8859-9',
            'WINDOWS1254' => 'Windows-1254', 'WINDOWS-1254' => 'Windows-1254', 'CP1254' => 'Windows-1254',
            'WINDOWS1252' => 'Windows-1252', 'WINDOWS-1252' => 'Windows-1252', 'CP1252' => 'Windows-1252',
            'ISO88591' => 'ISO-8859-1', 'ISO-8859-1' => 'ISO-8859-1', 'LATIN1' => 'ISO-8859-1',
            'UNKNOWN8BIT' => 'Windows-1254', 'UNKNOWN-8BIT' => 'Windows-1254',
        ];
        $charset = $aliases[$charset] ?? $charset;

        if ($charset === 'UTF-8' && mb_check_encoding($text, 'UTF-8')) {
            return $text;
        }

        $converted = @mb_convert_encoding($text, 'UTF-8', $charset);
        if ($converted === false || ! mb_check_encoding($converted, 'UTF-8')) {
            $converted = @mb_convert_encoding($text, 'UTF-8', 'Windows-1254');
        }

        return $converted === false ? $text : $converted;
    }

    private static function guessCharset(string $text): string
    {
        return mb_check_encoding($text, 'UTF-8') ? 'UTF-8' : 'Windows-1254';
    }

    /**
     * "Ad Soyad <a@b.com>, x@y.com" → [['name'=>..,'email'=>..], ...]
     *
     * @return array<int, array{name:string,email:string}>
     */
    public static function parseAddressList(?string $value): array
    {
        $value = trim((string) $value);
        if ($value === '' || strtoupper($value) === 'NIL') {
            return [];
        }

        // Tırnak içindeki virgülleri bölmemek için elle tarama
        $chunks = [];
        $buffer = '';
        $inQuote = false;
        $depth = 0;

        for ($i = 0, $len = strlen($value); $i < $len; $i++) {
            $ch = $value[$i];

            if ($ch === '"' && ($i === 0 || $value[$i - 1] !== '\\')) {
                $inQuote = ! $inQuote;
            } elseif (! $inQuote && $ch === '<') {
                $depth++;
            } elseif (! $inQuote && $ch === '>') {
                $depth = max(0, $depth - 1);
            } elseif (! $inQuote && $depth === 0 && ($ch === ',' || $ch === ';')) {
                $chunks[] = $buffer;
                $buffer = '';

                continue;
            }

            $buffer .= $ch;
        }
        $chunks[] = $buffer;

        $out = [];
        foreach ($chunks as $chunk) {
            $chunk = trim($chunk);
            if ($chunk === '') {
                continue;
            }

            $name = '';
            $email = '';

            if (preg_match('/^(.*)<([^>]*)>\s*$/s', $chunk, $m)) {
                $name = trim(trim($m[1]), '"\' ');
                $email = trim($m[2]);
            } else {
                $email = trim($chunk, '<> ');
            }

            $email = trim(str_replace(['"', "'"], '', $email));
            if ($email === '') {
                continue;
            }

            $name = self::decodeWords($name);
            $out[] = [
                'name' => $name !== '' ? $name : $email,
                'email' => mb_strtolower($email),
            ];
        }

        return $out;
    }

    public static function firstAddress(?string $value): array
    {
        $list = self::parseAddressList($value);

        return $list[0] ?? ['name' => '', 'email' => ''];
    }

    private static function cleanId(?string $value): ?string
    {
        $value = trim((string) $value);
        if ($value === '') {
            return null;
        }

        $value = explode("\n", $value)[0];

        return mb_substr(trim($value, '<> '), 0, 500) ?: null;
    }

    private static function parseReferences(string $value): array
    {
        preg_match_all('/<([^>]+)>/', $value, $m);

        return array_slice($m[1] ?? [], -25);
    }

    private static function parseDate(?string $value): ?Carbon
    {
        $value = trim((string) $value);
        if ($value === '') {
            return null;
        }

        // "(GMT+03:00)" gibi yorum eklerini at
        $value = trim((string) preg_replace('/\s*\(.*?\)\s*$/', '', $value));

        // Baştaki gün adını at: bazı istemciler yanlış gün yazar ve PHP'nin tarih
        // ayrıştırıcısı bu durumda tarihi o güne kaydırır.
        $value = trim((string) preg_replace('/^[A-Za-z]{3,9},\s*/', '', $value));

        $timezone = self::appTimezone();

        try {
            return Carbon::parse($value)->setTimezone($timezone);
        } catch (\Throwable) {
            $ts = @strtotime($value);

            return $ts ? Carbon::createFromTimestamp($ts, $timezone) : null;
        }
    }

    /** Uygulama saat dilimi — konteyner dışında da (testlerde) güvenli. */
    public static function appTimezone(): string
    {
        try {
            return config('app.timezone') ?: 'UTC';
        } catch (\Throwable) {
            return 'UTC';
        }
    }

    /** 1 = yüksek · 3 = normal · 5 = düşük */
    private static function parsePriority(array $headers): int
    {
        $raw = $headers['x-priority'] ?? $headers['importance'] ?? $headers['x-msmail-priority'] ?? '';
        $raw = mb_strtolower(trim($raw));

        if ($raw === '') {
            return 3;
        }
        if (str_starts_with($raw, '1') || str_starts_with($raw, '2') || str_contains($raw, 'high') || str_contains($raw, 'urgent')) {
            return 1;
        }
        if (str_starts_with($raw, '4') || str_starts_with($raw, '5') || str_contains($raw, 'low')) {
            return 5;
        }

        return 3;
    }

    // ─────────────────────────────────────────── gövde parçaları

    /**
     * MIME ağacını gezer; her yaprak parçayı $out dizisine ekler.
     */
    private static function walkPart(
        string $contentType,
        string $encoding,
        string $disposition,
        string $contentId,
        string $body,
        string $partId,
        array &$out,
        int $depth = 0
    ): void {
        if ($depth > 12) {
            return;
        }

        $mime = mb_strtolower(trim(explode(';', $contentType)[0] ?: 'text/plain'));

        if (str_starts_with($mime, 'multipart/')) {
            $boundary = self::paramValue($contentType, 'boundary');
            if ($boundary === null) {
                return;
            }

            $index = 0;
            foreach (self::splitBoundary($body, $boundary) as $section) {
                $index++;
                [$subHeadersRaw, $subBody] = self::splitMessage($section);
                $subHeaders = self::headerMap($subHeadersRaw);

                self::walkPart(
                    $subHeaders['content-type'] ?? 'text/plain',
                    $subHeaders['content-transfer-encoding'] ?? '7bit',
                    $subHeaders['content-disposition'] ?? '',
                    $subHeaders['content-id'] ?? '',
                    $subBody,
                    $partId === '' ? (string) $index : "$partId.$index",
                    $out,
                    $depth + 1
                );
            }

            return;
        }

        $decoded = self::decodeTransfer($body, $encoding);

        $filename = self::paramValue($disposition, 'filename') ?? self::paramValue($contentType, 'name');
        $filename = $filename !== null ? self::decodeWords($filename) : null;

        $dispositionType = mb_strtolower(trim(explode(';', $disposition)[0] ?: ''));
        $isInline = $dispositionType === 'inline' && $contentId !== '';
        $isAttachment = $dispositionType === 'attachment'
            || ($filename !== null && $filename !== '')
            || $isInline
            || (! str_starts_with($mime, 'text/') && $mime !== 'message/rfc822');

        if (! $isAttachment) {
            $decoded = self::toUtf8($decoded, self::paramValue($contentType, 'charset') ?: 'UTF-8');
        }

        $out[] = [
            'mime' => $mime,
            'content' => $decoded,
            'filename' => self::sanitizeFilename($filename ?? ''),
            'content_id' => trim($contentId, '<> '),
            'is_inline' => $isInline,
            'is_attachment' => $isAttachment,
            'part_id' => $partId,
        ];
    }

    /** Sınırlayıcıya göre multipart gövdesini parçalara ayırır. */
    private static function splitBoundary(string $body, string $boundary): array
    {
        $body = str_replace("\r\n", "\n", $body);
        $marker = '--'.$boundary;

        $pieces = explode($marker, $body);
        array_shift($pieces);              // sınırlayıcıdan önceki önsöz

        $sections = [];
        foreach ($pieces as $piece) {
            if (str_starts_with($piece, '--')) {   // kapanış sınırlayıcısı
                break;
            }
            $sections[] = ltrim($piece, "\n");
        }

        return $sections;
    }

    private static function decodeTransfer(string $body, string $encoding): string
    {
        return match (mb_strtolower(trim($encoding))) {
            'base64' => (string) base64_decode(preg_replace('/\s+/', '', $body) ?? '', false),
            'quoted-printable' => quoted_printable_decode($body),
            default => $body,
        };
    }

    /** Content-Type/Disposition parametresini okur (RFC 2231 devamlarını da birleştirir). */
    private static function paramValue(string $header, string $param): ?string
    {
        if ($header === '') {
            return null;
        }

        // Basit biçim: name="değer" veya name=değer
        if (preg_match('/;\s*'.preg_quote($param, '/').'\s*=\s*("([^"]*)"|([^;\s]+))/i', $header, $m)) {
            return $m[2] !== '' ? $m[2] : ($m[3] ?? null);
        }

        // RFC 2231: name*=UTF-8''deger  ·  name*0=..; name*1=..
        if (preg_match('/;\s*'.preg_quote($param, '/').'\*\s*=\s*([^;]+)/i', $header, $m)) {
            $value = trim($m[1], '"');
            if (preg_match("/^([^']*)'([^']*)'(.*)$/", $value, $mm)) {
                return self::toUtf8(rawurldecode($mm[3]), $mm[1]);
            }

            return rawurldecode($value);
        }

        if (preg_match_all('/;\s*'.preg_quote($param, '/').'\*\d+\*?\s*=\s*("([^"]*)"|([^;]+))/i', $header, $ms, PREG_SET_ORDER)) {
            $value = '';
            foreach ($ms as $m) {
                $value .= trim($m[2] !== '' ? $m[2] : ($m[3] ?? ''));
            }
            if (preg_match("/^([^']*)'([^']*)'(.*)$/", $value, $mm)) {
                return self::toUtf8(rawurldecode($mm[3]), $mm[1]);
            }

            return rawurldecode($value);
        }

        return null;
    }

    /** Dosya adından yol ayracı ve tehlikeli karakterleri temizler. */
    public static function sanitizeFilename(string $name): string
    {
        $name = str_replace(['\\', '/', "\0"], '', trim($name));
        $name = (string) preg_replace('/[\x00-\x1F\x7F]/u', '', $name);

        return mb_substr($name, 0, 190);
    }
}
