<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Iodev\Whois\Factory;
use Iodev\Whois\Whois;

class NetworkToolsService
{
    /**
     * PRC-163 SEC-Critical — allow-listed DNS record types.
     */
    private const ALLOWED_DNS_TYPES = ['A', 'AAAA', 'MX', 'TXT', 'NS', 'CNAME', 'SOA', 'PTR', 'SRV', 'CAA'];

    protected $whois;

    public function __construct()
    {
        $this->whois = Factory::get()->createWhois();
    }

    /**
     * Whois sorgusu yapar
     */
    public function whois(string $domain): array
    {
        try {
            // PRC-163: hostname validation — RCE'yi tüm public method'larda kapatıyoruz.
            if (! $this->isValidHostname($domain)) {
                return $this->invalidHostResponse($domain, 'whois');
            }

            // Cache kontrolü
            $cacheKey = 'whois_query_'.md5($domain);
            if (config('whois.cache.enabled') && Cache::has($cacheKey)) {
                return Cache::get($cacheKey);
            }

            // Domain'den TLD çıkar
            $tld = $this->extractTld($domain);

            // TLD config'ini al
            $tldConfig = $this->getTldConfig($tld);

            // Or define the same via assoc way
            $customServer = \Iodev\Whois\Modules\Tld\TldServer::fromData([
                'zone' => $tld,
                'host' => $tldConfig['server'] ?? config('whois.default_server'),
                'timeout' => $tldConfig['timeout'] ?? config('whois.default_timeout'),
            ]);

            // Add custom server to existing whois instance
            $this->whois->getTldModule()->addServers([$customServer]);

            // Whois sorgusu yap
            try {
                $info = $this->whois->loadDomainInfo($domain);

            } catch (\Exception $whoisException) {
                Log::error('Whois loadDomainInfo failed', [
                    'domain' => $domain,
                    'error' => $whoisException->getMessage(),
                    'tld' => $tld,
                    'tldConfig' => $tldConfig,
                ]);
                $info = null;
            }

            if ($info) {
                $result = [
                    'success' => true,
                    'domain' => $domain,
                    'tld' => $tld,
                    'server' => $tldConfig['server'] ?? config('whois.default_server'),
                    'registrar' => $info->getRegistrar(),
                    'creation_date' => $info->getCreationDate(),
                    'expiration_date' => $info->getExpirationDate(),
                    'status' => $info->getStates(),
                    'nameservers' => $info->getNameservers(),
                    'raw_output' => $this->getRawWhoisOutput($domain, $tldConfig['server'] ?? config('whois.default_server')),
                    'timestamp' => now(),
                ];
            } else {
                // Fallback olarak raw whois çıktısı al
                $rawOutput = $this->getRawWhoisOutput($domain, $tldConfig['server'] ?? config('whois.default_server'));
                $result = [
                    'success' => true,
                    'domain' => $domain,
                    'tld' => $tld,
                    'server' => $tldConfig['server'] ?? config('whois.default_server'),
                    'raw_output' => $rawOutput,
                    'timestamp' => now(),
                ];
            }

            // Cache'e kaydet
            if (config('whois.cache.enabled')) {
                Cache::put($cacheKey, $result, config('whois.cache.ttl'));
            }

            return $result;

        } catch (\Exception $e) {
            Log::error('Whois query failed', [
                'domain' => $domain,
                'error' => $e->getMessage(),
            ]);

            return [
                'success' => false,
                'error' => 'Whois sorgusu başarısız: '.$e->getMessage(),
                'domain' => $domain,
                'timestamp' => now(),
            ];
        }
    }

    /**
     * Domain'den TLD çıkar
     */
    protected function extractTld(string $domain): string
    {
        $parts = explode('.', $domain);
        if (count($parts) < 2) {
            return '.'.$domain;
        }

        // Önce son parçayı dene (.cc, .com, .net gibi)
        $lastPart = end($parts);
        $tld = '.'.$lastPart;

        // Eğer bu TLD config'de varsa, bunu kullan
        if (config('whois.tlds.'.$lastPart)) {
            return $tld;
        }

        // Yoksa son iki parçayı dene (.co.uk, .com.tr gibi)
        if (count($parts) >= 2) {
            $lastTwo = array_slice($parts, -2);
            $twoPartTld = '.'.implode('.', $lastTwo);

            // Eğer bu TLD config'de varsa, bunu kullan
            if (config('whois.tlds.'.implode('.', $lastTwo))) {
                return $twoPartTld;
            }
        }

        // Hiçbiri yoksa son parçayı döndür
        return $tld;
    }

    /**
     * TLD config'ini al
     */
    protected function getTldConfig(string $tld): array
    {
        // TLD'yi temizle (başındaki noktayı kaldır)
        $cleanTld = ltrim($tld, '.');

        // Önce tam TLD'yi ara (config'de .cc formatında)
        $config = config('whois.tlds.'.$tld);

        if (! $config) {
            // Tam TLD bulunamadıysa, noktasız versiyonu dene
            $config = config('whois.tlds.'.$cleanTld);
        }

        if (! $config) {
            // Hala bulunamadıysa, son parçayı dene
            $parts = explode('.', $cleanTld);
            $lastPart = end($parts);
            $config = config('whois.tlds.'.$lastPart);
        }

        if (! is_array($config)) {
            return [
                'server' => config('whois.default_server'),
            ];
        }

        return $config;
    }

    /**
     * Raw whois çıktısı al
     *
     * PRC-163: Process::run array form ile çağrılıyor → shell yok.
     * `$server` argümanı tldConfig'den geliyor (DB'den değil; saldırgan
     * kontrolünde değil), yine de array form ile escape garantili.
     */
    protected function getRawWhoisOutput(string $domain, string $server): string
    {
        try {
            $timeout = (int) config('whois.default_timeout');

            // `timeout` komutu Linux util-linux; macOS BSD `gtimeout` veya
            // yoksunsa Process timeout'una düşelim. Array form ile shell
            // injection imkansız.
            $result = Process::timeout($timeout + 5)
                ->run(['timeout', (string) $timeout, 'whois', '-h', $server, $domain]);

            if ($result->successful()) {
                return $result->output();
            } else {
                return __('admin/common.services_network_whois_error_raw').': '.$result->errorOutput();
            }
        } catch (\Exception $e) {
            return __('admin/common.services_network_whois_error_exception').': '.$e->getMessage();
        }
    }

    /**
     * Ping sorgusu yapar
     */
    public function ping(string $host, int $count = 4): array
    {
        if (! $this->isValidHostname($host)) {
            return $this->invalidHostResponse($host, 'ping') + ['count' => $count];
        }
        // PRC-163: count int cast'i validation'da yapılır; max 10 cap
        $count = max(1, min(10, $count));

        try {
            $result = Process::run(['ping', '-c', (string) $count, $host]);

            if ($result->successful()) {
                return [
                    'success' => true,
                    'output' => $result->output(),
                    'host' => $host,
                    'count' => $count,
                    'timestamp' => now(),
                ];
            } else {
                return [
                    'success' => false,
                    'error' => $result->errorOutput(),
                    'host' => $host,
                    'count' => $count,
                    'timestamp' => now(),
                ];
            }
        } catch (\Exception $e) {
            Log::error('Ping query failed', [
                'host' => $host,
                'count' => $count,
                'error' => $e->getMessage(),
            ]);

            return [
                'success' => false,
                'error' => 'Ping sorgusu başarısız: '.$e->getMessage(),
                'host' => $host,
                'count' => $count,
                'timestamp' => now(),
            ];
        }
    }

    /**
     * Traceroute sorgusu yapar
     */
    public function traceroute(string $host, int $maxHops = 30): array
    {
        if (! $this->isValidHostname($host)) {
            return $this->invalidHostResponse($host, 'traceroute') + ['max_hops' => $maxHops];
        }
        $maxHops = max(1, min(64, $maxHops));

        try {
            $result = Process::run(['traceroute', '-m', (string) $maxHops, $host]);

            if ($result->successful()) {
                return [
                    'success' => true,
                    'output' => $result->output(),
                    'host' => $host,
                    'max_hops' => $maxHops,
                    'timestamp' => now(),
                ];
            } else {
                return [
                    'success' => false,
                    'error' => $result->errorOutput(),
                    'host' => $host,
                    'max_hops' => $maxHops,
                    'timestamp' => now(),
                ];
            }
        } catch (\Exception $e) {
            Log::error('Traceroute query failed', [
                'host' => $host,
                'max_hops' => $maxHops,
                'error' => $e->getMessage(),
            ]);

            return [
                'success' => false,
                'error' => 'Traceroute sorgusu başarısız: '.$e->getMessage(),
                'host' => $host,
                'max_hops' => $maxHops,
                'timestamp' => now(),
            ];
        }
    }

    /**
     * DNS sorgusu yapar
     */
    public function dns(string $domain, string $type = 'A'): array
    {
        if (! $this->isValidHostname($domain)) {
            return $this->invalidHostResponse($domain, 'dns') + ['type' => $type];
        }
        $type = $this->normalizeDnsType($type);

        try {
            $result = Process::run(['dig', $type, $domain, '+short']);

            if ($result->successful()) {
                $output = trim($result->output());
                $records = $output ? explode("\n", $output) : [];

                return [
                    'success' => true,
                    'records' => $records,
                    'domain' => $domain,
                    'type' => $type,
                    'timestamp' => now(),
                ];
            } else {
                return [
                    'success' => false,
                    'error' => $result->errorOutput(),
                    'domain' => $domain,
                    'type' => $type,
                    'timestamp' => now(),
                ];
            }
        } catch (\Exception $e) {
            Log::error('DNS query failed', [
                'domain' => $domain,
                'type' => $type,
                'error' => $e->getMessage(),
            ]);

            return [
                'success' => false,
                'error' => 'DNS sorgusu başarısız: '.$e->getMessage(),
                'domain' => $domain,
                'type' => $type,
                'timestamp' => now(),
            ];
        }
    }

    /**
     * MX kayıt sorgusu yapar
     */
    public function mx(string $domain): array
    {
        if (! $this->isValidHostname($domain)) {
            return $this->invalidHostResponse($domain, 'mx');
        }

        try {
            $result = Process::run(['dig', 'MX', $domain, '+short']);

            if ($result->successful()) {
                $output = trim($result->output());
                $records = $output ? explode("\n", $output) : [];

                return [
                    'success' => true,
                    'records' => $records,
                    'domain' => $domain,
                    'timestamp' => now(),
                ];
            } else {
                return [
                    'success' => false,
                    'error' => $result->errorOutput(),
                    'domain' => $domain,
                    'timestamp' => now(),
                ];
            }
        } catch (\Exception $e) {
            Log::error('MX query failed', [
                'domain' => $domain,
                'error' => $e->getMessage(),
            ]);

            return [
                'success' => false,
                'error' => 'MX sorgusu başarısız: '.$e->getMessage(),
                'domain' => $domain,
                'timestamp' => now(),
            ];
        }
    }

    /**
     * TXT kayıt sorgusu yapar
     */
    public function txt(string $domain): array
    {
        if (! $this->isValidHostname($domain)) {
            return $this->invalidHostResponse($domain, 'txt');
        }

        try {
            $result = Process::run(['dig', 'TXT', $domain, '+short']);

            if ($result->successful()) {
                $output = trim($result->output());
                $records = $output ? explode("\n", $output) : [];

                return [
                    'success' => true,
                    'records' => $records,
                    'domain' => $domain,
                    'timestamp' => now(),
                ];
            } else {
                return [
                    'success' => false,
                    'error' => $result->errorOutput(),
                    'domain' => $domain,
                    'timestamp' => now(),
                ];
            }
        } catch (\Exception $e) {
            Log::error('TXT query failed', [
                'domain' => $domain,
                'error' => $e->getMessage(),
            ]);

            return [
                'success' => false,
                'error' => 'TXT sorgusu başarısız: '.$e->getMessage(),
                'domain' => $domain,
                'timestamp' => now(),
            ];
        }
    }

    /**
     * PRC-163: Strict hostname/IP validation. RFC 1123 + IPv4 + IPv6.
     * `evil.com; cat /etc/passwd`, `$(curl x)`, `|whoami` reddedilir.
     */
    private function isValidHostname(string $host): bool
    {
        $host = trim($host);
        if ($host === '' || strlen($host) > 253) {
            return false;
        }

        // IPv4
        if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            return true;
        }
        // IPv6
        if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            return true;
        }

        // RFC 1123 hostname: a-z 0-9 . - ; her label ≤63 char; sonu rakam/harf
        return (bool) preg_match('/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i', $host);
    }

    /**
     * PRC-163: DNS record type whitelist enforcement.
     */
    private function normalizeDnsType(string $type): string
    {
        $type = strtoupper(trim($type));
        return in_array($type, self::ALLOWED_DNS_TYPES, true) ? $type : 'A';
    }

    /**
     * Common error response for invalid hostnames — returned without spawning Process.
     */
    private function invalidHostResponse(string $host, string $operation): array
    {
        Log::warning('NetworkToolsService rejected invalid hostname', [
            'operation' => $operation,
            'host' => $host,
        ]);

        return [
            'success' => false,
            'error' => __('admin/common.services_network_invalid_host'),
            'host' => $host,
            'timestamp' => now(),
        ];
    }
}
