<?php

namespace App\Services\Mail;

use RuntimeException;

/**
 * Saf PHP IMAP4rev1 istemcisi.
 *
 * Sunucuda ext-imap kurulu olmadığı için protokol doğrudan stream soketi
 * üzerinden konuşulur. Desteklenen akış: bağlan → LOGIN → LIST/SELECT →
 * UID SEARCH/FETCH/STORE/COPY/MOVE/APPEND/EXPUNGE → LOGOUT.
 *
 * Yanıt okuma: IMAP literalleri ({n}\r\n + n bayt) satırdan sökülüp sırayla
 * saklanır, satırda yerlerine \x01LITn\x01 yer tutucusu bırakılır. Böylece
 * her mantıksal yanıt tek satırlık düz metin gibi regex ile ayrıştırılabilir.
 */
class ImapClient
{
    /** @var resource|null */
    private $stream = null;

    private int $tagSeq = 0;

    private array $capabilities = [];

    private ?string $selectedFolder = null;

    private bool $selectedReadOnly = false;

    /** Son SELECT sonucu (EXISTS/UIDNEXT/UIDVALIDITY/UNSEEN). */
    private array $selectedInfo = [];

    public function __construct(
        private string $host,
        private int $port = 993,
        private string $encryption = 'ssl',   // ssl | tls | none
        private bool $validateCert = true,
        private int $timeout = 20,
    ) {}

    // ─────────────────────────────────────────── bağlantı

    public function connect(): void
    {
        if ($this->stream) {
            return;
        }

        $transport = $this->encryption === 'ssl' ? 'ssl' : 'tcp';
        $context = stream_context_create(['ssl' => [
            'verify_peer' => $this->validateCert,
            'verify_peer_name' => $this->validateCert,
            'allow_self_signed' => ! $this->validateCert,
            'SNI_enabled' => true,
            'peer_name' => $this->host,
        ]]);

        $errNo = 0;
        $errStr = '';
        $stream = @stream_socket_client(
            "$transport://{$this->host}:{$this->port}",
            $errNo,
            $errStr,
            $this->timeout,
            STREAM_CLIENT_CONNECT,
            $context
        );

        if (! $stream) {
            throw new RuntimeException("IMAP bağlantısı kurulamadı ({$this->host}:{$this->port}): ".($errStr ?: 'bilinmeyen hata'));
        }

        stream_set_timeout($stream, $this->timeout);
        $this->stream = $stream;

        $greeting = $this->readLine();
        if (! str_starts_with(ltrim($greeting), '* OK') && ! str_starts_with(ltrim($greeting), '* PREAUTH')) {
            $this->disconnect();
            throw new RuntimeException('IMAP sunucusu bağlantıyı reddetti: '.trim($greeting));
        }

        if ($this->encryption === 'tls') {
            $this->command('STARTTLS');
            $crypto = @stream_socket_enable_crypto($this->stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
            if (! $crypto) {
                $this->disconnect();
                throw new RuntimeException('STARTTLS başarısız — şifreleme ayarını kontrol edin.');
            }
        }

        $this->loadCapabilities();
    }

    public function login(string $username, string $password): void
    {
        $this->connect();
        $this->command('LOGIN '.$this->quote($username).' '.$this->quote($password));
        // Giriş sonrası yetenek listesi değişebilir
        $this->loadCapabilities();
    }

    public function logout(): void
    {
        if (! $this->stream) {
            return;
        }
        try {
            $this->command('LOGOUT');
        } catch (\Throwable) {
            // kapanışta hata önemli değil
        }
        $this->disconnect();
    }

    public function disconnect(): void
    {
        if ($this->stream) {
            @fclose($this->stream);
            $this->stream = null;
        }
        $this->selectedFolder = null;
        $this->selectedInfo = [];
    }

    public function __destruct()
    {
        $this->disconnect();
    }

    public function hasCapability(string $cap): bool
    {
        return in_array(strtoupper($cap), $this->capabilities, true);
    }

    private function loadCapabilities(): void
    {
        $res = $this->command('CAPABILITY');
        foreach ($res['lines'] as $line) {
            if (preg_match('/^\*\s+CAPABILITY\s+(.+)$/i', trim($line['text']), $m)) {
                $this->capabilities = array_map('strtoupper', preg_split('/\s+/', trim($m[1])) ?: []);
            }
        }
    }

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

    /**
     * Sunucudaki tüm klasörler.
     *
     * @return array<int, array{path:string,name:string,delimiter:string,flags:array<int,string>,selectable:bool,type:string}>
     */
    public function listFolders(): array
    {
        $res = $this->command('LIST "" "*"');
        $folders = [];

        foreach ($res['lines'] as $line) {
            $text = $line['text'];
            if (! preg_match('/^\*\s+LIST\s+\(([^)]*)\)\s+(NIL|"[^"]*"|\S+)\s+(.+)$/i', trim($text), $m)) {
                continue;
            }

            $flags = $m[1] !== '' ? preg_split('/\s+/', trim($m[1])) : [];
            $flags = array_map('strtolower', $flags ?: []);
            $delimiter = trim($m[2], '"');
            $delimiter = $delimiter === 'NIL' ? '.' : $delimiter;

            // Yol literal olarak gelmiş olabilir; NUL yer tutucusu korunmalı
            $rawPath = $this->resolveToken(trim($m[3], " \t\r\n"), $line['literals']);
            $path = ImapUtf7::decode($rawPath);

            $segments = $delimiter !== '' ? explode($delimiter, $path) : [$path];
            $name = (string) end($segments);

            $folders[] = [
                'path' => $rawPath,          // sunucuya gönderilecek ham yol (mUTF-7)
                'display_path' => $path,     // insan okunur yol
                'name' => $name,
                'delimiter' => $delimiter ?: '.',
                'flags' => $flags,
                'selectable' => ! in_array('\noselect', $flags, true),
                'type' => $this->detectFolderType($path, $flags),
            ];
        }

        return $folders;
    }

    /**
     * Klasör tipini SPECIAL-USE bayrağından, olmazsa isimden çıkarır.
     * İsim eşlemesi Türkçe/İngilizce sunucu adlarını kapsar.
     */
    private function detectFolderType(string $path, array $flags): string
    {
        $byFlag = [
            '\sent' => 'sent', '\drafts' => 'drafts', '\trash' => 'trash',
            '\junk' => 'junk', '\archive' => 'archive', '\all' => 'archive',
        ];
        foreach ($byFlag as $flag => $type) {
            if (in_array($flag, $flags, true)) {
                return $type;
            }
        }

        $leaf = self::fold((string) preg_replace('/^.*[.\/]/u', '', $path));

        if (self::fold($path) === 'inbox') {
            return 'inbox';
        }

        // Türkçe/İngilizce yaygın klasör adları (karşılaştırma aksansız yapılır)
        $names = [
            'sent' => ['sent', 'sentitems', 'sentmessages', 'sentmail', 'gonderilmis', 'gonderilmisogeler', 'gidenkutusu', 'gonderilenler', 'gonderilen'],
            'drafts' => ['drafts', 'draft', 'taslaklar', 'taslak'],
            'trash' => ['trash', 'deleted', 'deleteditems', 'deletedmessages', 'cop', 'copkutusu', 'silinmisogeler', 'silinenler', 'silinmisler'],
            'junk' => ['junk', 'spam', 'bulkmail', 'junkemail', 'junkeposta', 'gereksiz', 'gereksizeposta', 'istenmeyen', 'istenmeyeneposta'],
            'archive' => ['archive', 'archives', 'arsiv', 'allmail'],
        ];

        foreach ($names as $type => $candidates) {
            if (in_array($leaf, $candidates, true)) {
                return $type;
            }
        }

        // "Sent_backup", "Konuşma Geçmişi" gibi türevler özel klasör kalır
        return 'custom';
    }

    /**
     * Klasör adını karşılaştırılabilir hâle getirir: küçük harf, Türkçe
     * karakterler ASCII karşılığına, boşluk/ayraç temizlenmiş.
     */
    private static function fold(string $value): string
    {
        $map = [
            'İ' => 'i', 'I' => 'i', 'ı' => 'i', 'Ş' => 's', 'ş' => 's',
            'Ğ' => 'g', 'ğ' => 'g', 'Ü' => 'u', 'ü' => 'u',
            'Ö' => 'o', 'ö' => 'o', 'Ç' => 'c', 'ç' => 'c',
        ];

        $value = strtr($value, $map);
        $value = mb_strtolower($value, 'UTF-8');

        return (string) preg_replace('/[^a-z0-9]/u', '', $value);
    }

    /**
     * Klasörü seçer; EXISTS / UIDNEXT / UIDVALIDITY / UNSEEN döner.
     *
     * Aynı klasör aynı kipte üst üste istendiğinde komut tekrarlanmaz — ham
     * ileti indirme gibi döngülerde her ileti için yeniden SELECT gitmesin.
     * Klasörü değiştiren her komut bu belleği düşürür (bkz. invalidateSelection).
     */
    public function select(string $rawPath, bool $readOnly = false): array
    {
        if ($this->selectedFolder === $rawPath && $this->selectedReadOnly === $readOnly && $this->selectedInfo) {
            return $this->selectedInfo;
        }

        $cmd = $readOnly ? 'EXAMINE' : 'SELECT';
        $res = $this->command($cmd.' '.$this->quote($rawPath));

        $info = ['exists' => 0, 'uidnext' => null, 'uidvalidity' => null, 'unseen' => 0, 'recent' => 0];
        foreach ($res['lines'] as $line) {
            $t = trim($line['text']);
            if (preg_match('/^\*\s+(\d+)\s+EXISTS/i', $t, $m)) {
                $info['exists'] = (int) $m[1];
            } elseif (preg_match('/^\*\s+(\d+)\s+RECENT/i', $t, $m)) {
                $info['recent'] = (int) $m[1];
            } elseif (preg_match('/UIDNEXT\s+(\d+)/i', $t, $m)) {
                $info['uidnext'] = (int) $m[1];
            } elseif (preg_match('/UIDVALIDITY\s+(\d+)/i', $t, $m)) {
                $info['uidvalidity'] = (int) $m[1];
            }
        }

        $this->selectedFolder = $rawPath;
        $this->selectedReadOnly = $readOnly;
        $this->selectedInfo = $info;

        return $info;
    }

    /** Klasörün içeriği değişti — bir sonraki select() yeniden sunucuya sorsun. */
    private function invalidateSelection(): void
    {
        $this->selectedInfo = [];
    }

    public function createFolder(string $rawPath): void
    {
        $this->command('CREATE '.$this->quote($rawPath));
    }

    public function deleteFolder(string $rawPath): void
    {
        $this->command('DELETE '.$this->quote($rawPath));
    }

    public function renameFolder(string $from, string $to): void
    {
        $this->command('RENAME '.$this->quote($from).' '.$this->quote($to));
    }

    // ─────────────────────────────────────────── arama & getirme

    /** @return int[] Klasördeki tüm UID'ler. */
    public function searchAll(): array
    {
        return $this->search('ALL');
    }

    /** Verilen tarihten itibaren gelen mesajların UID'leri. */
    public function searchSince(\DateTimeInterface $since): array
    {
        return $this->search('SINCE '.$since->format('d-M-Y'));
    }

    /** Sunucu tarafı arama (IMAP SEARCH ölçütü). */
    public function search(string $criteria): array
    {
        $res = $this->command('UID SEARCH '.$criteria);
        $uids = [];
        foreach ($res['lines'] as $line) {
            if (preg_match('/^\*\s+SEARCH(.*)$/i', trim($line['text']), $m)) {
                foreach (preg_split('/\s+/', trim($m[1])) ?: [] as $n) {
                    if ($n !== '' && ctype_digit($n)) {
                        $uids[] = (int) $n;
                    }
                }
            }
        }
        sort($uids);

        return $uids;
    }

    /**
     * Başlık + bayrak + boyut getirir (gövde indirilmez).
     *
     * @param  int[]  $uids
     * @return array<int, array{uid:int,flags:array<int,string>,size:int,headers:string,internal_date:?string}>
     */
    public function fetchHeaders(array $uids): array
    {
        if (! $uids) {
            return [];
        }

        $set = $this->compressUidSet($uids);
        $res = $this->command("UID FETCH $set (UID FLAGS RFC822.SIZE INTERNALDATE BODY.PEEK[HEADER])");

        $out = [];
        foreach ($res['lines'] as $line) {
            $text = $line['text'];
            if (! preg_match('/^\*\s+\d+\s+FETCH\s+\(/i', trim($text))) {
                continue;
            }
            if (! preg_match('/UID\s+(\d+)/i', $text, $mu)) {
                continue;
            }

            $uid = (int) $mu[1];
            $flags = [];
            if (preg_match('/FLAGS\s+\(([^)]*)\)/i', $text, $mf)) {
                $flags = array_values(array_filter(preg_split('/\s+/', trim($mf[1])) ?: []));
            }

            $size = preg_match('/RFC822\.SIZE\s+(\d+)/i', $text, $ms) ? (int) $ms[1] : 0;
            $internal = preg_match('/INTERNALDATE\s+"([^"]+)"/i', $text, $mi) ? $mi[1] : null;

            $headers = '';
            if (preg_match('/BODY\[HEADER[^\]]*\]\s+(\x01LIT\d+\x01|"(?:[^"\\\\]|\\\\.)*"|NIL)/i', $text, $mh)) {
                $headers = $this->resolveToken($mh[1], $line['literals']);
            }

            $out[$uid] = [
                'uid' => $uid,
                'flags' => $flags,
                'size' => $size,
                'headers' => $headers,
                'internal_date' => $internal,
            ];
        }

        return $out;
    }

    /** Yalnızca bayraklar — hızlı bayrak eşitlemesi için. */
    public function fetchFlags(array $uids): array
    {
        if (! $uids) {
            return [];
        }

        $res = $this->command('UID FETCH '.$this->compressUidSet($uids).' (UID FLAGS)');
        $out = [];
        foreach ($res['lines'] as $line) {
            $text = $line['text'];
            if (preg_match('/UID\s+(\d+)/i', $text, $mu) && preg_match('/FLAGS\s+\(([^)]*)\)/i', $text, $mf)) {
                $out[(int) $mu[1]] = array_values(array_filter(preg_split('/\s+/', trim($mf[1])) ?: []));
            }
        }

        return $out;
    }

    /** Tek mesajın ham RFC822 içeriği (gövde + ekler dahil). */
    public function fetchRaw(int $uid): ?string
    {
        $res = $this->command("UID FETCH $uid (BODY.PEEK[])");

        foreach ($res['lines'] as $line) {
            if (preg_match('/BODY\[\]\s+(\x01LIT\d+\x01|"(?:[^"\\\\]|\\\\.)*")/i', $line['text'], $m)) {
                return $this->resolveToken($m[1], $line['literals']);
            }
        }

        return null;
    }

    // ─────────────────────────────────────────── değiştirme

    /**
     * Bayrak ekler/çıkarır.
     *
     * @param  int[]  $uids
     * @param  string[]  $flags  ör. ['\Seen', '\Flagged']
     * @param  string  $mode  '+' ekle · '-' çıkar · '' birebir ata
     */
    public function storeFlags(array $uids, array $flags, string $mode = '+'): void
    {
        if (! $uids || ! $flags) {
            return;
        }

        $this->command(
            'UID STORE '.$this->compressUidSet($uids)." {$mode}FLAGS.SILENT (".implode(' ', $flags).')'
        );

        $this->invalidateSelection();
    }

    /** Mesajları başka klasöre taşır (MOVE yoksa COPY + \Deleted + EXPUNGE). */
    public function moveMessages(array $uids, string $destRawPath): void
    {
        if (! $uids) {
            return;
        }

        $set = $this->compressUidSet($uids);

        if ($this->hasCapability('MOVE')) {
            $this->command("UID MOVE $set ".$this->quote($destRawPath));
            $this->invalidateSelection();

            return;
        }

        $this->command("UID COPY $set ".$this->quote($destRawPath));
        $this->storeFlags($uids, ['\Deleted'], '+');
        $this->expunge();
    }

    /** \Deleted işaretli mesajları kalıcı olarak siler. */
    public function expunge(): void
    {
        $this->command('EXPUNGE');
        $this->invalidateSelection();
    }

    /** Kalıcı silme: \Deleted + EXPUNGE. */
    public function deleteMessages(array $uids): void
    {
        if (! $uids) {
            return;
        }
        $this->storeFlags($uids, ['\Deleted'], '+');
        $this->expunge();
    }

    /**
     * Ham mesajı klasöre ekler (gönderilen postayı Sent'e yazmak için).
     * Literal gönderimi: {n} → sunucu "+" ile devam isteyince gövde yazılır.
     */
    public function append(string $rawPath, string $rawMessage, array $flags = ['\Seen']): void
    {
        $this->requireStream();

        $rawMessage = preg_replace("/\r?\n/", "\r\n", $rawMessage) ?? $rawMessage;
        $len = strlen($rawMessage);
        $flagStr = $flags ? ' ('.implode(' ', $flags).')' : '';

        $tag = $this->nextTag();
        $this->write("$tag APPEND ".$this->quote($rawPath)."$flagStr {".$len."}\r\n");

        $line = $this->readLine();
        if (! str_starts_with(ltrim($line), '+')) {
            throw new RuntimeException('APPEND reddedildi: '.trim($line));
        }

        $this->write($rawMessage."\r\n");
        $this->readUntilTag($tag);

        $this->invalidateSelection();
    }

    // ─────────────────────────────────────────── protokol altyapısı

    /**
     * Komutu gönderir, etiketli yanıta kadar okur, OK değilse hata fırlatır.
     *
     * @return array{status:string,lines:array<int,array{text:string,literals:array<int,string>}>,final:string}
     */
    public function command(string $command): array
    {
        $this->requireStream();

        $tag = $this->nextTag();
        $this->write("$tag $command\r\n");

        return $this->readUntilTag($tag, $command);
    }

    private function readUntilTag(string $tag, string $command = ''): array
    {
        $lines = [];
        $guard = 0;

        while (true) {
            if (++$guard > 200000) {
                throw new RuntimeException('IMAP yanıtı beklenenden uzun sürdü.');
            }

            [$text, $literals] = $this->readLogicalLine();

            if (preg_match('/^'.preg_quote($tag, '/').'\s+(OK|NO|BAD)\b(.*)$/i', trim($text), $m)) {
                $status = strtoupper($m[1]);
                if ($status !== 'OK') {
                    $safe = preg_replace('/^LOGIN\s+.*/i', 'LOGIN ***', $command);
                    throw new RuntimeException(trim($m[2]).($safe ? " (komut: $safe)" : ''));
                }

                return ['status' => $status, 'lines' => $lines, 'final' => trim($text)];
            }

            $lines[] = ['text' => $text, 'literals' => $literals];
        }
    }

    /**
     * Tek mantıksal yanıt satırı okur. Satırdaki {n} literalleri ayrı okunup
     * dizide toplanır, satırda \x01LITn\x01 yer tutucusu kalır.
     *
     * Sınır baytı olarak bilerek \x01 seçildi: trim()'in varsayılan karakter
     * listesi ("\0" dahil) bu baytı kırpmaz, dolayısıyla yer tutucu araya giren
     * bir trim() çağrısıyla bozulamaz. IMAP protokol metninde \x01 geçmez.
     *
     * @return array{0:string,1:array<int,string>}
     */
    private function readLogicalLine(): array
    {
        $literals = [];
        $buffer = $this->readLine();

        while (preg_match('/\{(\d+)\}\r?\n$/', $buffer, $m)) {
            $bytes = $this->readBytes((int) $m[1]);
            $index = count($literals);
            $literals[] = $bytes;

            $buffer = substr($buffer, 0, -strlen($m[0]))."\x01LIT{$index}\x01";
            $buffer .= $this->readLine();
        }

        return [rtrim($buffer, "\r\n"), $literals];
    }

    /** Yer tutucuyu / tırnaklı dizgeyi gerçek değere çevirir. */
    private function resolveToken(string $token, array $literals): string
    {
        $token = trim($token, " \t\r\n");

        if (preg_match('/^\x01LIT(\d+)\x01$/', $token, $m)) {
            return $literals[(int) $m[1]] ?? '';
        }

        if ($token === 'NIL') {
            return '';
        }

        if (str_starts_with($token, '"') && str_ends_with($token, '"')) {
            return stripslashes(substr($token, 1, -1));
        }

        return $token;
    }

    private function readLine(): string
    {
        $this->requireStream();

        $line = fgets($this->stream, 16384);
        if ($line === false) {
            $meta = stream_get_meta_data($this->stream);
            $this->disconnect();
            throw new RuntimeException($meta['timed_out'] ?? false
                ? 'IMAP sunucusu yanıt vermedi (zaman aşımı).'
                : 'IMAP bağlantısı beklenmedik şekilde kapandı.');
        }

        // Uzun satırlar 16 KB'lık okumaya sığmayabilir; satır sonuna kadar devam et
        while (! str_ends_with($line, "\n")) {
            $more = fgets($this->stream, 16384);
            if ($more === false) {
                break;
            }
            $line .= $more;
        }

        return $line;
    }

    private function readBytes(int $length): string
    {
        $this->requireStream();

        $data = '';
        while (strlen($data) < $length) {
            $chunk = fread($this->stream, min(32768, $length - strlen($data)));
            if ($chunk === false || $chunk === '') {
                $meta = stream_get_meta_data($this->stream);
                if ($meta['timed_out'] ?? false) {
                    throw new RuntimeException('IMAP veri aktarımı zaman aşımına uğradı.');
                }
                break;
            }
            $data .= $chunk;
        }

        return $data;
    }

    private function write(string $data): void
    {
        $this->requireStream();

        $written = @fwrite($this->stream, $data);
        if ($written === false) {
            throw new RuntimeException('IMAP sunucusuna yazılamadı.');
        }
    }

    private function requireStream(): void
    {
        if (! $this->stream) {
            throw new RuntimeException('IMAP bağlantısı açık değil.');
        }
    }

    private function nextTag(): string
    {
        return 'A'.str_pad((string) (++$this->tagSeq), 5, '0', STR_PAD_LEFT);
    }

    private function quote(string $value): string
    {
        return '"'.str_replace(['\\', '"'], ['\\\\', '\\"'], $value).'"';
    }

    /** [1,2,3,7,9,10] → "1:3,7,9:10" (uzun UID listelerini kısaltır). */
    private function compressUidSet(array $uids): string
    {
        $uids = array_values(array_unique(array_map('intval', $uids)));
        sort($uids);

        $ranges = [];
        $start = $prev = null;

        foreach ($uids as $uid) {
            if ($start === null) {
                $start = $prev = $uid;

                continue;
            }
            if ($uid === $prev + 1) {
                $prev = $uid;

                continue;
            }
            $ranges[] = $start === $prev ? (string) $start : "$start:$prev";
            $start = $prev = $uid;
        }

        if ($start !== null) {
            $ranges[] = $start === $prev ? (string) $start : "$start:$prev";
        }

        return implode(',', $ranges);
    }
}
