<?php

declare(strict_types=1);

namespace App\Application\Admin;

use App\Domain\Time\Clock;
use App\Infrastructure\Reputation\DashboardStatsRepository;
use DateTimeImmutable;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
 * `GET /api/v1/admin/stats/dashboard` — aggregate counters + 24h
 * histogram + top reporters/categories + jobs status.
 *
 * In-process 30 s cache. Multi-replica deployments will see brief
 * staleness across replicas — accepted; mirrors the BlocklistCache
 * semantics from M07.
 *
 * RBAC: Viewer.
 */
final class StatsController
{
    use AdminControllerSupport;

    /** @var array<string, mixed>|null */
    private ?array $cached = null;
    private ?float $cacheExpiresAt = null;
    private const TTL_SECONDS = 30.0;

    /**
     * Expected job intervals (seconds). The status payload flips
     * `overdue` when a job's `last_finished_at` is older than its
     * interval. Mirrors the values backed by the env vars from §9.
     *
     * @var array<string, int>
     */
    private const EXPECTED_INTERVALS = [
        'recompute-scores' => 600,        // 10 min
        'cleanup-audit' => 86400,         // 1 day
        'enrich-pending' => 3600,         // 1 hour
        'refresh-geoip' => 7 * 86400,     // 1 week
    ];

    public function __construct(
        private readonly DashboardStatsRepository $stats,
        private readonly Clock $clock,
    ) {
    }

    public function dashboard(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $now = $this->clock->now();
        $nowFloat = (float) $now->getTimestamp();
        if ($this->cached !== null && $this->cacheExpiresAt !== null && $nowFloat < $this->cacheExpiresAt) {
            return self::json($response, 200, $this->cached);
        }

        $since = $now->modify('-24 hours');
        $weekAgo = $now->modify('-7 days');

        $payload = [
            'active_blocks' => $this->stats->activeBlocksApprox(),
            'manual_blocks_count' => $this->stats->manualBlocksCount(),
            'allowlist_count' => $this->stats->allowlistCount(),
            'reports_24h' => $this->stats->reportsSince($since),
            'reports_24h_by_hour' => $this->stats->reportsByHourSince($since),
            'top_reporters_24h' => $this->stats->topReportersSince($since),
            'top_categories_24h' => $this->stats->topCategoriesSince($since),
            'blocked_ips_by_day_7d' => self::buildBlockedSeries(
                $this->stats->blockedIpsByDayCategorySince($weekAgo),
                $this->stats->activeCategorySlugs(),
                $weekAgo,
                $now,
            ),
            'jobs_status' => $this->stats->jobsStatus(self::EXPECTED_INTERVALS, $now),
            'reference_policy' => 'moderate',
        ];

        $this->cached = $payload;
        $this->cacheExpiresAt = $nowFloat + self::TTL_SECONDS;

        return self::json($response, 200, $payload);
    }

    /**
     * Pivot a sparse `(day, category, count)` triple stream into the
     * stacked-bar shape consumed by the dashboard chart.
     *
     * The output guarantees:
     *  - `days` covers every calendar day in `[from, to]`, in order.
     *  - `series` contains one entry per category that either appears
     *    in the rows or in `activeCategories` (so categories with zero
     *    activity still render as flat zero bars instead of vanishing).
     *  - Each series carries a counts array aligned 1:1 with `days`.
     *
     * @param list<array{day: string, category: string, count: int}> $rows
     * @param list<string>                                            $activeCategories
     * @return array{days: list<string>, series: list<array{category: string, counts: list<int>}>}
     */
    private static function buildBlockedSeries(
        array $rows,
        array $activeCategories,
        DateTimeImmutable $from,
        DateTimeImmutable $to,
    ): array {
        $days = [];
        $cursor = $from->setTime(0, 0, 0);
        $end = $to->setTime(0, 0, 0);
        while ($cursor <= $end) {
            $days[] = $cursor->format('Y-m-d');
            $cursor = $cursor->modify('+1 day');
        }

        $byCategoryDay = [];
        $seenCategories = [];
        foreach ($rows as $row) {
            $cat = $row['category'];
            $seenCategories[$cat] = true;
            $byCategoryDay[$cat][$row['day']] = $row['count'];
        }

        // Combine seen + active so categories without any reports still
        // appear in the legend; sort for stable order across renders.
        $allCategories = array_keys(array_merge(
            array_flip($activeCategories),
            $seenCategories,
        ));
        sort($allCategories);

        $series = [];
        foreach ($allCategories as $cat) {
            $counts = [];
            foreach ($days as $day) {
                $counts[] = (int) ($byCategoryDay[$cat][$day] ?? 0);
            }
            $series[] = ['category' => $cat, 'counts' => $counts];
        }

        return ['days' => $days, 'series' => $series];
    }
}
