<?php

declare(strict_types=1);

namespace App\Domain\Reputation;

use App\Domain\Time\Clock;
use App\Infrastructure\Category\CategoryRepository;
use App\Infrastructure\Reputation\ReportRepository;
use DateTimeImmutable;
use RuntimeException;

/**
 * Computes the score for one (ip, category) pair by summing weight × decay
 * across reports inside the hard cutoff window.
 *
 * SPEC §5: `score(X, C) = Σ weight × decay(now − received_at, C)`. The
 * cutoff is `SCORE_REPORT_HARD_CUTOFF_DAYS` (default 365) — older reports
 * are skipped for performance.
 *
 * The bulk recompute job in M05 will reuse this class so the synchronous
 * and batch paths can't drift.
 */
final class PairScorer
{
    public function __construct(
        private readonly ReportRepository $reports,
        private readonly CategoryRepository $categories,
        private readonly Clock $clock,
        private readonly int $hardCutoffDays = 365,
    ) {
    }

    public function score(string $ipBin, int $categoryId, ?DateTimeImmutable $now = null): float
    {
        $category = $this->categories->findById($categoryId);
        if ($category === null) {
            throw new RuntimeException(sprintf('Unknown category id %d', $categoryId));
        }

        $now ??= $this->clock->now();
        $cutoff = $now->modify(sprintf('-%d days', $this->hardCutoffDays));

        $rows = $this->reports->forScoring($ipBin, $categoryId, $cutoff);

        $score = 0.0;
        foreach ($rows as $row) {
            $ageDays = ($now->getTimestamp() - $row['received_at']->getTimestamp()) / 86400.0;
            $score += $row['weight'] * Decay::value($category->decayFunction, $ageDays, $category->decayParam);
        }

        return $score;
    }
}
