<?php
namespace Crawl\Services\Crawler;

class CurlMultiFetcher
{
    public function fetchMany(array $urls, int $timeoutSeconds, string $userAgent): array
    {
        $mh = curl_multi_init();
        $handles = [];
        $results = [];

        foreach ($urls as $key => $url) {
            $ch = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_MAXREDIRS => 5,
                CURLOPT_TIMEOUT => $timeoutSeconds,
                CURLOPT_CONNECTTIMEOUT => 10,
                CURLOPT_USERAGENT => $userAgent,
                CURLOPT_ENCODING => '',
                CURLOPT_SSL_VERIFYPEER => false,
                CURLOPT_SSL_VERIFYHOST => false,
                CURLOPT_HEADER => true,
            ]);

            curl_multi_add_handle($mh, $ch);
            $handles[$key] = $ch;
        }

        $running = null;
        do {
            do {
                $multiExec = curl_multi_exec($mh, $running);
            } while ($multiExec === CURLM_CALL_MULTI_PERFORM);

            if ($running > 0) {
                $selected = curl_multi_select($mh, 0.5);
                if ($selected === -1) {
                    // Avoid busy looping when select cannot watch descriptors.
                    usleep(100000);
                }
            }
        } while ($running > 0 && $multiExec === CURLM_OK);

        foreach ($handles as $key => $ch) {
            $raw = curl_multi_getcontent($ch);
            $info = curl_getinfo($ch);
            $error = curl_error($ch);
            $errno = curl_errno($ch);

            $headerSize = $info['header_size'] ?? 0;
            $headerText = substr($raw, 0, $headerSize);
            $body = substr($raw, $headerSize);

            $headers = [];
            foreach (explode("\r\n", $headerText) as $line) {
                if (str_contains($line, ':')) {
                    [$hKey, $hVal] = explode(':', $line, 2);
                    $headers[strtolower(trim($hKey))] = trim($hVal);
                }
            }

            if ($errno !== 0 && $error === '') {
                $error = 'cURL error #' . $errno;
            }
            if ((int)($info['http_code'] ?? 0) <= 0 && ($raw === '' || $raw === false) && $error === '') {
                $error = 'No HTTP response received';
            }

            // Extract HTTP version and redirect type from raw header blob.
            // The blob contains one status line per response hop (e.g. 301 + 200 for a redirect).
            $httpVersion  = 'HTTP/1.1';
            $redirectType = null;
            $statusLines  = [];
            foreach (explode("\r\n", $headerText) as $hLine) {
                if (preg_match('/^HTTP\/(\S+)\s+(\d{3})/i', $hLine, $hm)) {
                    $statusLines[] = ['version' => $hm[1], 'code' => (int)$hm[2]];
                }
            }
            if (!empty($statusLines)) {
                $httpVersion = 'HTTP/' . end($statusLines)['version'];
                if (count($statusLines) > 1) {
                    $redirectType = (string)$statusLines[0]['code'];
                }
            }

            $results[$key] = [
                'body'          => $body,
                'status_code'   => (int)($info['http_code'] ?? 0),
                'content_type'  => $info['content_type'] ?? ($headers['content-type'] ?? null),
                'response_time' => (float)($info['total_time'] ?? 0),
                'effective_url' => $info['url'] ?? $urls[$key],
                'headers'       => $headers,
                'error'         => $error ?: null,
                'http_version'  => $httpVersion,
                'redirect_type' => $redirectType,
            ];

            curl_multi_remove_handle($mh, $ch);
            curl_close($ch);
        }

        curl_multi_close($mh);
        return $results;
    }
}
