<?php

declare(strict_types=1);

namespace App\Application\Internal;

use App\Application\Jobs\CleanupAuditJob;
use App\Application\Jobs\CleanupExpiredManualBlocksJob;
use App\Application\Jobs\EnrichPendingJob;
use App\Application\Jobs\RecomputeScoresJob;
use App\Application\Jobs\RefreshGeoipJob;
use App\Application\Jobs\TickJob;
use App\Domain\Jobs\JobOutcome;
use App\Domain\Time\Clock;
use App\Infrastructure\Enrichment\Downloaders\GeoIpDownloader;
use App\Infrastructure\Jobs\JobLockRepository;
use App\Infrastructure\Jobs\JobRegistry;
use App\Infrastructure\Jobs\JobRunner;
use App\Infrastructure\Jobs\JobRunRepository;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
 * `/internal/jobs/*` controller — invoked only by the scheduler (cron /
 * sidecar). Network + token middlewares run upstream; this class handles
 * job dispatch and the response envelope.
 *
 * `refreshGeoip` returns 412 because the actual MaxMind plumbing lands in
 * M11. The endpoint exists now so the scheduler config doesn't need to
 * change between milestones.
 */
final class JobsController
{
    public function __construct(
        private readonly JobRegistry $registry,
        private readonly JobRunner $runner,
        private readonly JobRunRepository $runs,
        private readonly JobLockRepository $locks,
        private readonly Clock $clock,
        private readonly GeoIpDownloader $geoipDownloader,
    ) {
    }

    public function recomputeScores(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $body = self::jsonBody($request);
        $params = [];
        if (isset($body['full'])) {
            $params['full'] = (bool) $body['full'];
        }
        if (isset($body['max_rows']) && is_numeric($body['max_rows'])) {
            $params['max_rows'] = (int) $body['max_rows'];
        }

        $job = $this->registry->get(RecomputeScoresJob::NAME);
        $outcome = $this->runner->run($job, $params, 'schedule');

        return self::renderOutcome($response, $outcome);
    }

    public function cleanupAudit(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $job = $this->registry->get(CleanupAuditJob::NAME);
        $outcome = $this->runner->run($job, [], 'schedule');

        return self::renderOutcome($response, $outcome);
    }

    public function cleanupExpiredManualBlocks(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $job = $this->registry->get(CleanupExpiredManualBlocksJob::NAME);
        $outcome = $this->runner->run($job, [], 'schedule');

        return self::renderOutcome($response, $outcome);
    }

    public function enrichPending(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $job = $this->registry->get(EnrichPendingJob::NAME);
        $outcome = $this->runner->run($job, [], 'schedule');

        return self::renderOutcome($response, $outcome);
    }

    public function tick(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $job = $this->registry->get(TickJob::NAME);
        $outcome = $this->runner->run($job, [], 'schedule');

        return self::renderOutcome($response, $outcome);
    }

    public function refreshGeoip(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        // Opt-in providers without their credential: short-circuit before
        // taking the lock or starting the job. DB-IP needs no credential
        // and never reaches this branch.
        if ($this->geoipDownloader->requiresCredential() && !$this->geoipDownloader->hasCredential()) {
            $missing = match ($this->geoipDownloader->name()) {
                'maxmind' => 'MAXMIND_LICENSE_KEY',
                'ipinfo' => 'IPINFO_TOKEN',
                default => 'CREDENTIAL',
            };
            $response = $response->withStatus(412)->withHeader('Content-Type', 'application/json');
            $response->getBody()->write((string) json_encode([
                'error' => 'no_credential',
                'provider' => $this->geoipDownloader->name(),
                'missing' => $missing,
            ]));

            return $response;
        }

        $query = $request->getQueryParams();
        $params = [];
        if (isset($query['reenrich'])) {
            $params['reenrich'] = filter_var($query['reenrich'], FILTER_VALIDATE_BOOL);
        }
        if (isset($query['dry_run']) && filter_var($query['dry_run'], FILTER_VALIDATE_BOOL)) {
            // dry_run only confirms credentials/config — used by tests so the
            // controller can answer "would 412 fire?" without actually
            // pulling 100 MB of MMDBs over the wire.
            $response = $response->withStatus(202)->withHeader('Content-Type', 'application/json');
            $response->getBody()->write((string) json_encode([
                'job' => self::nameForRefresh(),
                'status' => 'success',
                'items_processed' => 0,
                'duration_ms' => 0,
                'run_id' => null,
                'details' => [
                    'provider' => $this->geoipDownloader->name(),
                    'dry_run' => true,
                ],
            ]));

            return $response;
        }

        $job = $this->registry->get(RefreshGeoipJob::NAME);
        $outcome = $this->runner->run($job, $params, 'schedule');

        return self::renderOutcome($response, $outcome);
    }

    private static function nameForRefresh(): string
    {
        return RefreshGeoipJob::NAME;
    }

    public function status(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $now = $this->clock->now();
        $latest = $this->runs->latestPerJob();

        $jobs = [];
        foreach ($this->registry->all() as $name => $job) {
            $row = $latest[$name] ?? null;

            $finishedAt = $row['finished_at'] ?? null;
            $overdue = $row === null
                || ($finishedAt instanceof \DateTimeImmutable
                    && ($now->getTimestamp() - $finishedAt->getTimestamp()) > $job->defaultIntervalSeconds());

            $jobs[$name] = [
                'name' => $name,
                'default_interval_seconds' => $job->defaultIntervalSeconds(),
                'max_runtime_seconds' => $job->maxRuntimeSeconds(),
                'overdue' => $overdue,
                'lock' => $this->locks->status($name),
                'last_run' => $row === null ? null : [
                    'id' => $row['id'],
                    'status' => $row['status'],
                    'items_processed' => $row['items_processed'],
                    'triggered_by' => $row['triggered_by'],
                    'started_at' => self::formatTs($row['started_at']),
                    'finished_at' => self::formatTs($row['finished_at']),
                    'error_message' => $row['error_message'],
                ],
            ];
        }

        $response = $response->withStatus(200)->withHeader('Content-Type', 'application/json');
        $response->getBody()->write((string) json_encode([
            'now' => $now->format('Y-m-d\TH:i:s\Z'),
            'jobs' => $jobs,
        ]));

        return $response;
    }

    private static function renderOutcome(ResponseInterface $response, JobOutcome $outcome): ResponseInterface
    {
        $response = $response
            ->withStatus($outcome->httpStatus())
            ->withHeader('Content-Type', 'application/json');
        $response->getBody()->write((string) json_encode($outcome->toArray()));

        return $response;
    }

    /**
     * @return array<string, mixed>
     */
    private static function jsonBody(ServerRequestInterface $request): array
    {
        $parsed = $request->getParsedBody();
        if (is_array($parsed)) {
            return $parsed;
        }
        $raw = (string) $request->getBody();
        if ($raw === '') {
            return [];
        }
        $decoded = json_decode($raw, true);

        return is_array($decoded) ? $decoded : [];
    }

    private static function formatTs(mixed $value): ?string
    {
        if (!$value instanceof \DateTimeImmutable) {
            return null;
        }

        return $value->format('Y-m-d\TH:i:s\Z');
    }
}
