<?php

declare(strict_types=1);

namespace App\Domain\Email\Services;

use Illuminate\Support\Facades\Cache;

/**
 * SPF / DKIM / DMARC / MX deliverability checker.
 *
 * dns_get_record() yerleşik PHP fonksiyonu kullanır — sysapi gerektirmez,
 * Linux ve Windows üzerinde çalışır. Sonuçlar 5 dakika cachelenir.
 *
 * Çıktı formatı her record için:
 *   ['found' => bool, 'value' => string|null, 'valid' => bool, 'issues' => string[]]
 *
 * overall_score (0-100):
 *   SPF:30 + DKIM:35 + DMARC:35 = 100
 *   valid yerine sadece found ise 50% kredit
 */
final class DeliverabilityChecker
{
    private const CACHE_TTL_SECONDS = 300;

    private const DKIM_SELECTOR = 'default';

    /**
     * Bir domain için tüm deliverability kontrollerini yap.
     *
     * @return array{
     *   spf: array{found:bool,value:?string,valid:bool,issues:string[]},
     *   dkim: array{found:bool,selector:string,value:?string,valid:bool,issues:string[]},
     *   dmarc: array{found:bool,value:?string,policy:?string,valid:bool,issues:string[]},
     *   mx: array{found:bool,records:array<int,array{priority:int,host:string}>,valid:bool,issues:string[]},
     *   overall_score: int
     * }
     */
    public function check(string $domain): array
    {
        $domain = strtolower(trim($domain));

        return Cache::remember(
            "deliverability:{$domain}",
            self::CACHE_TTL_SECONDS,
            fn (): array => $this->performChecks($domain),
        );
    }

    /**
     * Cache'i bypass eden direkt kontrol (admin force-refresh için).
     *
     * @return array<string, mixed>
     */
    public function forceCheck(string $domain): array
    {
        $domain = strtolower(trim($domain));
        Cache::forget("deliverability:{$domain}");

        return $this->check($domain);
    }

    /**
     * @return array<string, mixed>
     */
    private function performChecks(string $domain): array
    {
        $spf = $this->checkSpf($domain);
        $dkim = $this->checkDkim($domain);
        $dmarc = $this->checkDmarc($domain);
        $mx = $this->checkMx($domain);
        $ptr = $this->checkPtr();
        $rbl = $this->checkRbl();

        // Score weights: SPF 25, DKIM 25, DMARC 25, MX 10, PTR 10, RBL 5 = 100
        $score = 0;
        $score += $spf['valid'] ? 25 : ($spf['found'] ? 12 : 0);
        $score += $dkim['valid'] ? 25 : ($dkim['found'] ? 12 : 0);
        $score += $dmarc['valid'] ? 25 : ($dmarc['found'] ? 12 : 0);
        $score += $mx['valid'] ? 10 : 0;
        $score += $ptr['valid'] ? 10 : ($ptr['found'] ? 5 : 0);
        $score += $rbl['valid'] ? 5 : 0;

        return [
            'spf' => $spf,
            'dkim' => $dkim,
            'dmarc' => $dmarc,
            'mx' => $mx,
            'ptr' => $ptr,
            'rbl' => $rbl,
            'overall_score' => $score,
        ];
    }

    /**
     * Reverse DNS (PTR) kontrolü — sunucu IP'sinin hostname'i mail FQDN'iyle uyumlu mu?
     * Gmail/Yandex SMTP rejection için kritik.
     *
     * @return array{found:bool,server_ip:?string,ptr:?string,expected:?string,valid:bool,issues:string[]}
     */
    public function checkPtr(): array
    {
        $serverIp = (string) (config('onoxsoft.mail.public_ipv4') ?? config('onoxsoft.public_ipv4') ?? '');
        $expectedHost = (string) (config('onoxsoft.mail.smtp_hostname') ?? config('onoxsoft.hostname') ?? gethostname() ?? '');

        if ($serverIp === '' || $serverIp === '0.0.0.0') {
            return [
                'found' => false,
                'server_ip' => null,
                'ptr' => null,
                'expected' => $expectedHost,
                'valid' => false,
                'issues' => ['Sunucu IP yapılandırılmadı (config/onoxsoft.php → mail.public_ipv4).'],
            ];
        }

        // IPv4 → ters notasyon (1.2.3.4 → 4.3.2.1.in-addr.arpa)
        $parts = explode('.', $serverIp);
        if (count($parts) !== 4) {
            return [
                'found' => false,
                'server_ip' => $serverIp,
                'ptr' => null,
                'expected' => $expectedHost,
                'valid' => false,
                'issues' => ['IPv4 formatı geçersiz.'],
            ];
        }
        $reverse = implode('.', array_reverse($parts)) . '.in-addr.arpa';
        $records = $this->dnsGetRecord($reverse, DNS_PTR);

        if (empty($records)) {
            return [
                'found' => false,
                'server_ip' => $serverIp,
                'ptr' => null,
                'expected' => $expectedHost,
                'valid' => false,
                'issues' => [
                    "PTR kaydı yok ({$serverIp} için reverse DNS). " .
                    "Hosting sağlayıcına başvurup PTR'yi {$expectedHost} olarak set ettir.",
                ],
            ];
        }

        $ptr = rtrim((string) ($records[0]['target'] ?? ''), '.');
        $valid = $ptr !== '' && $expectedHost !== ''
            && (strcasecmp($ptr, $expectedHost) === 0 || str_ends_with(strtolower($ptr), strtolower('.' . $expectedHost)));
        $issues = [];
        if (! $valid && $ptr !== '') {
            $issues[] = "PTR ({$ptr}) beklenen hostname ({$expectedHost}) ile uyuşmuyor — Gmail/Yandex spam'e atabilir.";
        }

        return [
            'found' => true,
            'server_ip' => $serverIp,
            'ptr' => $ptr,
            'expected' => $expectedHost,
            'valid' => $valid,
            'issues' => $issues,
        ];
    }

    /**
     * Public RBL listelerinde sunucu IP'sini sorgula (Spamhaus, SpamCop, Barracuda).
     *
     * @return array{checked:array<int,array{name:string,zone:string,listed:bool,error:?string}>,server_ip:?string,listed_count:int,valid:bool,issues:string[]}
     */
    public function checkRbl(): array
    {
        $serverIp = (string) (config('onoxsoft.mail.public_ipv4') ?? config('onoxsoft.public_ipv4') ?? '');

        if ($serverIp === '' || $serverIp === '0.0.0.0') {
            return [
                'checked' => [],
                'server_ip' => null,
                'listed_count' => 0,
                'valid' => false,
                'issues' => ['Sunucu IP yapılandırılmadı.'],
            ];
        }

        $parts = explode('.', $serverIp);
        if (count($parts) !== 4) {
            return [
                'checked' => [],
                'server_ip' => $serverIp,
                'listed_count' => 0,
                'valid' => false,
                'issues' => ['IPv4 formatı geçersiz.'],
            ];
        }
        $reverseIp = implode('.', array_reverse($parts));

        $zones = [
            ['name' => 'Spamhaus ZEN', 'zone' => 'zen.spamhaus.org'],
            ['name' => 'SpamCop', 'zone' => 'bl.spamcop.net'],
            ['name' => 'Barracuda', 'zone' => 'b.barracudacentral.org'],
            ['name' => 'SORBS', 'zone' => 'dnsbl.sorbs.net'],
            ['name' => 'UCEPROTECT-1', 'zone' => 'dnsbl-1.uceprotect.net'],
        ];

        $checked = [];
        $listedCount = 0;
        $issues = [];

        foreach ($zones as $rbl) {
            $lookup = $reverseIp . '.' . $rbl['zone'];
            // gethostbyname yerine dns_get_record DNS_A — daha güvenilir + suppress edilebilir
            $records = $this->dnsGetRecord($lookup, DNS_A);
            $listed = ! empty($records);
            if ($listed) {
                $listedCount++;
                $issues[] = "{$rbl['name']} listelemiş — kara liste kaldırma için " . $rbl['zone'] . " sitesinden delist talep et.";
            }
            $checked[] = [
                'name' => $rbl['name'],
                'zone' => $rbl['zone'],
                'listed' => $listed,
                'error' => null,
            ];
        }

        return [
            'checked' => $checked,
            'server_ip' => $serverIp,
            'listed_count' => $listedCount,
            'valid' => $listedCount === 0,
            'issues' => $issues,
        ];
    }

    /**
     * @return array{found:bool,value:?string,valid:bool,issues:string[]}
     */
    public function checkSpf(string $domain): array
    {
        $records = $this->getTxtRecords($domain);
        $issues = [];

        foreach ($records as $rec) {
            $txt = $rec['txt'] ?? $rec['entries'][0] ?? '';
            if (str_starts_with($txt, 'v=spf1')) {
                $valid = true;

                if (! preg_match('/[~\-+?]all\b/', $txt)) {
                    $issues[] = 'SPF "all" mekanizması yok — politika belirsiz.';
                    $valid = false;
                }
                if (substr_count($txt, 'v=spf1') > 1) {
                    $issues[] = 'Birden fazla v=spf1 — DNS standardına aykırı.';
                    $valid = false;
                }

                return [
                    'found' => true,
                    'value' => $txt,
                    'valid' => $valid,
                    'issues' => $issues,
                ];
            }
        }

        return [
            'found' => false,
            'value' => null,
            'valid' => false,
            'issues' => ['SPF kaydı bulunamadı. v=spf1 ile başlayan TXT eklemelisin.'],
        ];
    }

    /**
     * @return array{found:bool,selector:string,value:?string,valid:bool,issues:string[]}
     */
    public function checkDkim(string $domain, string $selector = self::DKIM_SELECTOR): array
    {
        $host = $selector . '._domainkey.' . $domain;
        $records = $this->getTxtRecords($host);
        $issues = [];

        foreach ($records as $rec) {
            $txt = $rec['txt'] ?? $rec['entries'][0] ?? '';
            if (str_contains($txt, 'v=DKIM1') || str_contains($txt, 'k=') || str_contains($txt, 'p=')) {
                $valid = true;
                if (! preg_match('/\bp=[A-Za-z0-9+\/=]{20,}/', $txt)) {
                    $issues[] = 'DKIM public key (p=) eksik veya çok kısa.';
                    $valid = false;
                }

                return [
                    'found' => true,
                    'selector' => $selector,
                    'value' => $txt,
                    'valid' => $valid,
                    'issues' => $issues,
                ];
            }
        }

        return [
            'found' => false,
            'selector' => $selector,
            'value' => null,
            'valid' => false,
            'issues' => ["DKIM kaydı bulunamadı ({$selector}._domainkey)."],
        ];
    }

    /**
     * @return array{found:bool,value:?string,policy:?string,valid:bool,issues:string[]}
     */
    public function checkDmarc(string $domain): array
    {
        $host = '_dmarc.' . $domain;
        $records = $this->getTxtRecords($host);
        $issues = [];

        foreach ($records as $rec) {
            $txt = $rec['txt'] ?? $rec['entries'][0] ?? '';
            if (str_starts_with($txt, 'v=DMARC1')) {
                $policy = null;
                if (preg_match('/\bp=(none|quarantine|reject)\b/i', $txt, $m)) {
                    $policy = strtolower($m[1]);
                }
                $valid = $policy !== null;
                if (! $valid) {
                    $issues[] = 'DMARC p= (policy) eksik.';
                }
                if ($policy === 'none') {
                    $issues[] = 'Policy "none" sadece monitör — quarantine/reject önerilir.';
                }

                return [
                    'found' => true,
                    'value' => $txt,
                    'policy' => $policy,
                    'valid' => $valid,
                    'issues' => $issues,
                ];
            }
        }

        return [
            'found' => false,
            'value' => null,
            'policy' => null,
            'valid' => false,
            'issues' => ['DMARC kaydı bulunamadı (_dmarc.' . $domain . ').'],
        ];
    }

    /**
     * @return array{found:bool,records:array<int,array{priority:int,host:string}>,valid:bool,issues:string[]}
     */
    public function checkMx(string $domain): array
    {
        $records = $this->dnsGetRecord($domain, DNS_MX);
        $mxList = [];

        foreach ($records as $rec) {
            if (($rec['type'] ?? '') !== 'MX') {
                continue;
            }
            $mxList[] = [
                'priority' => (int) ($rec['pri'] ?? 10),
                'host' => rtrim((string) ($rec['target'] ?? ''), '.'),
            ];
        }

        usort($mxList, static fn ($a, $b): int => $a['priority'] <=> $b['priority']);

        return [
            'found' => $mxList !== [],
            'records' => $mxList,
            'valid' => $mxList !== [],
            'issues' => $mxList === [] ? ['MX kaydı yok — bu domaine mail teslim edilemez.'] : [],
        ];
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function getTxtRecords(string $host): array
    {
        return $this->dnsGetRecord($host, DNS_TXT);
    }

    /**
     * dns_get_record() etrafında suppress + array guarantee. PHP, bazı OS'larda
     * DNS lookup başarısızlığında false döner; bunu boş array'e çeviriyoruz.
     *
     * @return array<int, array<string, mixed>>
     */
    private function dnsGetRecord(string $host, int $type): array
    {
        if (! function_exists('dns_get_record')) {
            return [];
        }

        // 4 sn'ye kadar bekle (PHP default 5 sn × 3 deneme = 15sn — bu çok uzun).
        // Maalesef dns_get_record timeout opsiyonu yok; system resolver'a bağlı.
        $result = @dns_get_record($host, $type);

        return is_array($result) ? $result : [];
    }
}
