<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use RuntimeException;

class RegistryWhoisService
{
    private ?array $rules = null;

    public function check(string $domain): array
    {
        $domain = $this->normalizeDomain($domain);
        $ttl = max(0, (int) config('zt.domain_lookup.cache_ttl', 300));
        $cacheKey = 'zt_registry_whois:'.sha1($domain);

        if ($ttl > 0 && Cache::has($cacheKey)) {
            $cached = Cache::get($cacheKey);
            if (is_array($cached)) {
                $cached['source'] = 'registry-cache';
                return $cached;
            }
        }

        $rule = $this->findRule($domain);
        if (! $rule) {
            return [
                'status' => 'unknown',
                'source' => 'registry-unsupported',
                'raw' => null,
                'supported' => false,
            ];
        }

        $response = str_starts_with(strtolower((string) $rule['uri']), 'socket://')
            ? $this->querySocket((string) $rule['uri'], $domain)
            : $this->queryHttp((string) $rule['uri'], $domain);

        if ($this->looksLikeFailure($response)) {
            throw new RuntimeException('The registry did not return a usable response.');
        }

        $availableMarker = strtolower((string) $rule['available']);
        $available = str_contains(strtolower($response), $availableMarker);
        $result = [
            'status' => $available ? 'available' : 'unavailable',
            'source' => 'registry-whois',
            'raw' => null,
            'supported' => true,
        ];

        if ($ttl > 0) {
            Cache::put($cacheKey, $result, $ttl);
        }

        return $result;
    }

    public function checkMany(array $domains): array
    {
        $results = [];

        foreach (array_values(array_unique($domains)) as $domain) {
            try {
                $normalized = $this->normalizeDomain((string) $domain);
                $results[$normalized] = $this->check($normalized);
            } catch (\Throwable) {
                $normalized = strtolower(trim((string) $domain));
                $results[$normalized] = [
                    'status' => 'unknown',
                    'source' => 'registry-error',
                    'raw' => null,
                    'supported' => false,
                ];
            }
        }

        return $results;
    }

    private function rules(): array
    {
        if ($this->rules !== null) {
            return $this->rules;
        }

        $files = [
            resource_path('data/domain-whois/whois.json'),
            resource_path('data/domain-whois/dist.whois.json'),
        ];
        $rulesByExtension = [];

        foreach ($files as $file) {
            if (! is_file($file)) {
                continue;
            }

            $decoded = json_decode((string) file_get_contents($file), true);
            if (! is_array($decoded)) {
                continue;
            }

            foreach ($decoded as $rule) {
                if (! is_array($rule) || empty($rule['extensions']) || empty($rule['uri']) || empty($rule['available'])) {
                    continue;
                }

                foreach (explode(',', (string) $rule['extensions']) as $extension) {
                    $extension = strtolower(trim($extension));
                    if ($extension === '') {
                        continue;
                    }
                    if (! str_starts_with($extension, '.')) {
                        $extension = '.'.$extension;
                    }

                    // Custom rules are loaded first and therefore take precedence.
                    $rulesByExtension[$extension] ??= $rule;
                }
            }
        }

        uksort($rulesByExtension, fn (string $a, string $b) => strlen($b) <=> strlen($a));

        return $this->rules = $rulesByExtension;
    }

    private function findRule(string $domain): ?array
    {
        foreach ($this->rules() as $extension => $rule) {
            if (str_ends_with($domain, $extension)) {
                return $rule;
            }
        }

        return null;
    }

    private function querySocket(string $uri, string $domain): string
    {
        $target = preg_replace('~^socket://~i', '', $uri) ?: '';
        $host = $target;
        $port = 43;
        $lastColon = strrpos($target, ':');

        if ($lastColon !== false && ctype_digit(substr($target, $lastColon + 1))) {
            $host = substr($target, 0, $lastColon);
            $port = (int) substr($target, $lastColon + 1);
        }

        $timeout = max(1, (int) config('zt.domain_lookup.socket_timeout', 5));
        $socket = @stream_socket_client(
            'tcp://'.$host.':'.$port,
            $errorNumber,
            $errorMessage,
            $timeout,
            STREAM_CLIENT_CONNECT
        );

        if (! is_resource($socket)) {
            throw new RuntimeException('Could not connect to the domain registry.');
        }

        stream_set_blocking($socket, false);
        fwrite($socket, $domain."\r\n");

        $response = '';
        $deadline = microtime(true) + $timeout;
        $idleTimeout = max(200, (int) config('zt.domain_lookup.socket_idle_timeout_ms', 700)) / 1000;

        while (microtime(true) < $deadline && strlen($response) < 256000) {
            $remaining = max(0.0, min($idleTimeout, $deadline - microtime(true)));
            $seconds = (int) floor($remaining);
            $microseconds = (int) (($remaining - $seconds) * 1000000);
            $read = [$socket];
            $write = null;
            $except = null;
            $ready = @stream_select($read, $write, $except, $seconds, $microseconds);

            if ($ready === false) {
                break;
            }

            if ($ready === 0) {
                // WHOIS servers usually close the connection, but some leave it
                // open after sending the complete response. Once data has begun,
                // a short idle period is enough to finish without waiting for the
                // full socket timeout.
                if ($response !== '') {
                    break;
                }

                continue;
            }

            $chunk = fread($socket, 8192);
            if ($chunk === false) {
                break;
            }

            if ($chunk !== '') {
                $response .= $chunk;
            }

            if (feof($socket)) {
                break;
            }
        }

        fclose($socket);

        if (trim($response) === '') {
            throw new RuntimeException('The domain registry lookup timed out or returned no data.');
        }

        return $response;
    }

    private function queryHttp(string $uri, string $domain): string
    {
        $timeout = max(1, (int) config('zt.domain_lookup.http_timeout', 6));
        $response = Http::withHeaders([
            'User-Agent' => 'Mozilla/5.0 (compatible; ZTHostingDomainCheck/1.0)',
            'Accept' => 'text/plain,text/html,application/json;q=0.9,*/*;q=0.8',
        ])->connectTimeout(min($timeout, 3))->timeout($timeout)->get($uri.rawurlencode($domain));

        if (! $response->successful()) {
            throw new RuntimeException('The domain registry lookup failed.');
        }

        return $response->body();
    }

    private function looksLikeFailure(string $response): bool
    {
        $text = strtolower(trim($response));
        if ($text === '') {
            return true;
        }

        foreach ([
            'rate limit', 'too many requests', 'temporarily unavailable',
            'service unavailable', 'connection refused', 'access denied',
            'forbidden', 'internal server error', 'query limit exceeded',
        ] as $marker) {
            if (str_contains($text, $marker)) {
                return true;
            }
        }

        return false;
    }

    private function normalizeDomain(string $domain): string
    {
        $domain = strtolower(trim($domain));
        $domain = preg_replace('~^https?://~i', '', $domain) ?: $domain;
        $domain = preg_replace('~^www\.~i', '', $domain) ?: $domain;
        $domain = explode('/', $domain)[0];
        $domain = rtrim($domain, '.');

        if (! str_contains($domain, '.')) {
            $domain .= '.com';
        }

        if (strlen($domain) > 253 || ! preg_match('/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i', $domain)) {
            throw new RuntimeException('Enter a valid domain name.');
        }

        return $domain;
    }
}
