<?php

declare(strict_types=1);

namespace App\Domain\MailLogs\Services;

/**
 * MailLogReader — Postfix / Dovecot / Rspamd log dosyalarını tail eder.
 *
 * Log dosyaları:
 *   - Postfix:  /var/log/maillog (RHEL) | /var/log/mail.log (Debian)
 *   - Dovecot:  aynı dosya (Postfix ile birlikte syslog'a yazar)
 *   - Rspamd:   /var/log/rspamd/rspamd.log
 *
 * Performance: dosyayı tamamen okumayı önlemek için son N byte'tan başlar.
 */
class MailLogReader
{
    private const MAX_BYTES = 524288;  // 512KB (yaklaşık 5000 satır)

    /** @var array<string, array<string, string>> */
    private const LOG_PATHS = [
        'mail' => [
            'rhel' => '/var/log/maillog',
            'debian' => '/var/log/mail.log',
        ],
        'rspamd' => [
            'rhel' => '/var/log/rspamd/rspamd.log',
            'debian' => '/var/log/rspamd/rspamd.log',
        ],
    ];

    /**
     * Log dosyası bilgisi (var mı, boyutu, son değişiklik).
     */
    public function info(string $source = 'mail'): array
    {
        $path = $this->resolvePath($source);

        if (! $path || ! is_readable($path)) {
            return [
                'source' => $source,
                'path' => $path,
                'exists' => false,
                'readable' => false,
                'size_bytes' => 0,
                'modified_at' => null,
            ];
        }

        return [
            'source' => $source,
            'path' => $path,
            'exists' => true,
            'readable' => true,
            'size_bytes' => filesize($path),
            'size_human' => $this->humanBytes(filesize($path)),
            'modified_at' => date('c', filemtime($path)),
        ];
    }

    /**
     * Son N satır oku — varsayılan 200.
     * @return array<int, array{ts: string|null, level: string, message: string, raw: string}>
     */
    public function tail(string $source = 'mail', int $lines = 200, ?string $search = null): array
    {
        $path = $this->resolvePath($source);
        if (! $path || ! is_readable($path)) {
            return [];
        }

        $content = $this->readLastBytes($path, self::MAX_BYTES);
        $allLines = explode("\n", trim($content));
        $allLines = array_filter($allLines);

        // Filtre + parse
        $parsed = [];
        foreach (array_reverse($allLines) as $line) {
            if ($search && ! str_contains(strtolower($line), strtolower($search))) {
                continue;
            }
            $parsed[] = $this->parseLine($line);
            if (count($parsed) >= $lines) {
                break;
            }
        }

        return $parsed;
    }

    /**
     * Son N byte'ı oku — dosya ne kadar büyük olursa olsun.
     */
    private function readLastBytes(string $path, int $maxBytes): string
    {
        $size = filesize($path);
        if ($size <= $maxBytes) {
            return (string) @file_get_contents($path);
        }

        $fp = @fopen($path, 'r');
        if (! $fp) {
            return '';
        }

        fseek($fp, -$maxBytes, SEEK_END);
        // İlk yarım satırı atla (boundary)
        fgets($fp);
        $content = stream_get_contents($fp);
        fclose($fp);

        return (string) $content;
    }

    /**
     * Bir log satırını parse et — syslog format.
     */
    private function parseLine(string $line): array
    {
        $level = 'info';
        $ts = null;

        // Syslog format: "May 14 19:18:23 hostname service[pid]: message"
        if (preg_match('/^([A-Z][a-z]{2}\s+\d+\s+\d+:\d+:\d+)/', $line, $m)) {
            $ts = $m[1];
        }

        // Seviye tahmini — keyword bazlı
        $lower = strtolower($line);
        if (str_contains($lower, 'error') || str_contains($lower, 'fatal') || str_contains($lower, 'reject')) {
            $level = 'error';
        } elseif (str_contains($lower, 'warning') || str_contains($lower, 'warn') || str_contains($lower, 'deferred')) {
            $level = 'warn';
        } elseif (str_contains($lower, 'spam') || str_contains($lower, 'discard')) {
            $level = 'warn';
        }

        return [
            'ts' => $ts,
            'level' => $level,
            'message' => substr($line, strlen($ts ?? '') + 1),
            'raw' => $line,
        ];
    }

    /**
     * OS'a göre log path.
     */
    private function resolvePath(string $source): ?string
    {
        if (! isset(self::LOG_PATHS[$source])) {
            return null;
        }
        $osKey = PHP_OS_FAMILY === 'Linux' && file_exists('/etc/debian_version') ? 'debian' : 'rhel';

        return self::LOG_PATHS[$source][$osKey] ?? null;
    }

    private function humanBytes(int $bytes): string
    {
        if ($bytes < 1024) {
            return $bytes . ' B';
        }
        if ($bytes < 1024 * 1024) {
            return round($bytes / 1024, 1) . ' KB';
        }
        if ($bytes < 1024 * 1024 * 1024) {
            return round($bytes / 1024 / 1024, 1) . ' MB';
        }
        return round($bytes / 1024 / 1024 / 1024, 1) . ' GB';
    }

    /**
     * Postfix delivery events parser — `/var/log/maillog` içinden teslimat olaylarını çıkarır.
     *
     * Postfix iki ayrı satıra yazar: `from=<>` (qmgr) ve `to=<> status=...` (smtp).
     * Aynı queue_id ile eşleştirip tek event olarak döner. Mail Delivery Reports
     * sayfasının veri kaynağı.
     *
     * @return array<int, array{
     *   id: string, timestamp: string, sender: string, recipient: string,
     *   subject: string, status: string, smtp_code: string, smtp_message: string,
     *   size_bytes: int, host: string, delay_seconds: int
     * }>
     */
    public function parseDeliveryEvents(int $limit = 500, int $maxBytes = 2_097_152): array
    {
        $path = $this->resolvePath('mail');
        if (! $path || ! is_readable($path)) {
            return [];
        }

        $content = $this->readLastBytes($path, $maxBytes);
        $lines = explode("\n", trim($content));

        // queue_id → event aggregator
        $events = [];
        $hostname = gethostname() ?: 'localhost';

        foreach ($lines as $line) {
            if ($line === '') {
                continue;
            }

            // Timestamp prefix: "May 14 19:18:23"
            if (! preg_match('/^([A-Z][a-z]{2}\s+\d+\s+\d{2}:\d{2}:\d{2})/', $line, $tsMatch)) {
                continue;
            }
            $ts = $tsMatch[1];

            // Pattern A: qmgr from=<>, size=, nrcpt=
            // "postfix/qmgr[1234]: ABCDEF123: from=<user@host>, size=1234, nrcpt=1 (queue active)"
            if (preg_match(
                '/postfix\/(?:qmgr|cleanup)\[\d+\]:\s+([A-F0-9]+):\s+from=<([^>]*)>(?:.*size=(\d+))?/',
                $line,
                $m,
            )) {
                $queueId = $m[1];
                if (! isset($events[$queueId])) {
                    $events[$queueId] = $this->blankEvent($queueId, $ts, $hostname);
                }
                $events[$queueId]['sender'] = $m[2] !== '' ? $m[2] : '<>';
                if (! empty($m[3])) {
                    $events[$queueId]['size_bytes'] = (int) $m[3];
                }
                continue;
            }

            // Pattern B: smtp/local/lmtp to=<>, ... status=...
            // "postfix/smtp[1234]: ABCDEF123: to=<u@x>, relay=mx[1.2.3.4]:25, delay=0.5, dsn=2.0.0, status=sent (250 2.0.0 OK)"
            if (preg_match(
                '/postfix\/(?:smtp|local|lmtp|virtual|pipe)\[\d+\]:\s+([A-F0-9]+):\s+to=<([^>]+)>'
                .'(?:.*?delay=([\d.]+))?'
                .'.*?status=(\w+)(?:\s+\(([^)]*)\))?/',
                $line,
                $m,
            )) {
                $queueId = $m[1];
                if (! isset($events[$queueId])) {
                    $events[$queueId] = $this->blankEvent($queueId, $ts, $hostname);
                }
                $events[$queueId]['recipient'] = $m[2];
                if (! empty($m[3])) {
                    $events[$queueId]['delay_seconds'] = (int) round((float) $m[3]);
                }
                $events[$queueId]['status'] = $this->normalizeStatus((string) $m[4]);
                $events[$queueId]['timestamp'] = $this->toIso8601($ts);

                // SMTP response inside parens: "250 2.0.0 OK"
                if (! empty($m[5]) && preg_match('/^(\d{3})\s+(.*)/', $m[5], $rm)) {
                    $events[$queueId]['smtp_code'] = $rm[1];
                    $events[$queueId]['smtp_message'] = substr($rm[2], 0, 200);
                }
                continue;
            }

            // Pattern C: bounce/reject with explicit dsn
            // "postfix/smtpd[1234]: NOQUEUE: reject: RCPT from ... 550 5.1.1 ... from=<x> to=<y>"
            if (preg_match(
                '/postfix\/\w+\[\d+\]:\s+NOQUEUE:\s+reject:.*?\s(\d{3})\s+(\d\.\d\.\d)\s+(.*?)\s+from=<([^>]*)>\s+to=<([^>]+)>/',
                $line,
                $m,
            )) {
                $queueId = 'NOQUEUE-'.substr(hash('crc32', $line), 0, 8);
                $events[$queueId] = $this->blankEvent($queueId, $ts, $hostname);
                $events[$queueId]['sender'] = $m[4];
                $events[$queueId]['recipient'] = $m[5];
                $events[$queueId]['status'] = 'rejected';
                $events[$queueId]['smtp_code'] = $m[1];
                $events[$queueId]['smtp_message'] = substr($m[3], 0, 200);
                $events[$queueId]['timestamp'] = $this->toIso8601($ts);
            }
        }

        // Sadece sender + recipient'ı bilinen tam event'leri tut
        $valid = array_filter($events, fn ($e) => $e['sender'] !== '' && $e['recipient'] !== '');

        // Timestamp desc
        usort($valid, fn ($a, $b) => strcmp($b['timestamp'], $a['timestamp']));

        return array_slice(array_values($valid), 0, $limit);
    }

    /** @return array<string, mixed> */
    private function blankEvent(string $queueId, string $ts, string $host): array
    {
        return [
            'id' => $queueId,
            'timestamp' => $this->toIso8601($ts),
            'sender' => '',
            'recipient' => '',
            'subject' => '',         // Postfix log subject yazmaz; cleanup phase eklenirse alınabilir
            'status' => 'unknown',
            'smtp_code' => '',
            'smtp_message' => '',
            'size_bytes' => 0,
            'host' => $host,
            'delay_seconds' => 0,
        ];
    }

    private function normalizeStatus(string $raw): string
    {
        $lower = strtolower($raw);
        return match ($lower) {
            'sent', 'forwarded' => 'delivered',
            'deferred' => 'deferred',
            'bounced' => 'bounced',
            'expired' => 'bounced',
            'rejected' => 'rejected',
            'discard' => 'rejected',
            default => $lower,
        };
    }

    /** Syslog "May 14 19:18:23" → ISO 8601 (current year). */
    private function toIso8601(string $syslogTs): string
    {
        try {
            $dt = \Carbon\Carbon::createFromFormat('M j H:i:s', $syslogTs);
            if ($dt === false) {
                return now()->toIso8601String();
            }
            // Year not in syslog format — assume current year; rollback to prev if future
            $dt->year(now()->year);
            if ($dt->isFuture()) {
                $dt->subYear();
            }
            return $dt->toIso8601String();
        } catch (\Throwable) {
            return now()->toIso8601String();
        }
    }

    /**
     * Hızlı istatistikler — son N satırdan basit aggregate.
     */
    public function stats(string $source = 'mail', int $sampleLines = 500): array
    {
        $tail = $this->tail($source, $sampleLines);

        $counts = ['info' => 0, 'warn' => 0, 'error' => 0];
        $servicePatterns = [
            'postfix/smtpd' => 0,
            'postfix/smtp' => 0,
            'postfix/qmgr' => 0,
            'postfix/cleanup' => 0,
            'dovecot' => 0,
            'rspamd' => 0,
        ];

        foreach ($tail as $entry) {
            $counts[$entry['level']]++;
            foreach ($servicePatterns as $pattern => &$count) {
                if (str_contains($entry['raw'], $pattern)) {
                    $count++;
                }
            }
        }

        return [
            'sample_lines' => count($tail),
            'levels' => $counts,
            'services' => array_filter($servicePatterns, fn ($c) => $c > 0),
        ];
    }
}
