<?php

declare(strict_types=1);

namespace Autogedal\FeedMonitor\Model\Fetch;

use Autogedal\FeedMonitor\Model\Clock;
use Autogedal\FeedMonitor\Model\Config;
use Autogedal\FeedMonitor\Model\Problem;

/**
 * Reads a feed over HTTP with connect / per-attempt timeouts and a few attempts, within a limit for the whole read
 * (attempts and pauses included): each attempt gets at most the time left. Only temporary failures are retried
 * (connection errors, timeouts, HTTP 5xx / 429); a 404 or 403 is final.
 */
class FeedFetcher
{
    private const RETRY_HTTP_CODES = [408, 425, 429, 500, 502, 503, 504];

    public function __construct(
        private readonly HttpClient $httpClient,
        private readonly Config $config,
        private readonly Clock $clock
    ) {
    }

    /**
     * @param array<int, mixed> $curlOptions extra CURLOPT_* values (e.g. a user agent)
     */
    public function fetch(string $url, array $curlOptions = []): FetchResult
    {
        $attempts = $this->config->getAttempts();
        $limit = $this->config->getMaxTotalSeconds();
        $deadline = $this->clock->now() + $limit;
        $result = null;
        for ($attempt = 1; $attempt <= $attempts; $attempt++) {
            if ($attempt > 1) {
                $this->httpClient->sleep(min($this->config->getRetryDelay() * ($attempt - 1), max(0, $deadline - $this->clock->now())));
                $left = $deadline - $this->clock->now();
                if ($left < 1) {
                    return FetchResult::failed($url, (string)$result->getProblem(), sprintf(
                        '%s; oprit dupa limita totala de %d s',
                        $result->getError(),
                        $limit
                    ), $result->getHttpCode(), $attempt - 1);
                }
            }
            $left = max(1, $deadline - $this->clock->now());
            $response = $this->httpClient->get(
                $url,
                min($this->config->getConnectTimeout(), $left),
                min($this->config->getTimeout(), $left),
                $curlOptions
            );
            [$result, $retry] = $this->evaluate($url, $response, $attempt);
            if (!$retry) {
                break;
            }
        }

        return $result;
    }

    /**
     * @param array{body: string|false, http_code: int, error: string, errno: int} $response
     * @return array{0: FetchResult, 1: bool} result, worth retrying
     */
    private function evaluate(string $url, array $response, int $attempt): array
    {
        $code = $response['http_code'];
        if ($response['errno'] !== 0 || $response['body'] === false) {
            return [FetchResult::failed($url, Problem::FETCH, 'Eroare conexiune: ' . $response['error'], $code, $attempt), true];
        }
        if ($code >= 400) {
            return [
                FetchResult::failed($url, Problem::FETCH, 'HTTP ' . $code, $code, $attempt),
                in_array($code, self::RETRY_HTTP_CODES, true),
            ];
        }
        if (trim((string)$response['body']) === '') {
            return [FetchResult::empty($url, $code, $attempt), false];
        }

        return [FetchResult::ok($url, (string)$response['body'], $code, $attempt), false];
    }
}
