<?php

declare(strict_types=1);

namespace App\Application\Jobs;

use App\Domain\Audit\AuditAction;
use App\Domain\Audit\AuditContext;
use App\Domain\Audit\AuditEmitter;
use App\Domain\Jobs\Job;
use App\Domain\Jobs\JobContext;
use App\Domain\Jobs\JobResult;
use App\Domain\ManualBlock\ManualBlock;
use App\Infrastructure\ManualBlock\ManualBlockRepository;
use App\Infrastructure\Reputation\BlocklistCache;
use App\Infrastructure\Reputation\CidrEvaluatorFactory;

/**
 * Daily prune of expired `manual_blocks` rows.
 *
 * Read-time filtering (M14.5) already hides expired rows from every list,
 * lookup, and the CidrEvaluator's snapshot — so correctness doesn't depend
 * on this job. The job exists for tidiness: keeps the table from growing
 * unbounded with old expired entries.
 *
 * For each row deleted, emits one `manual_block.deleted` audit row with
 * `actor_kind=system` so the audit trail records who removed the block.
 * Invalidates the CidrEvaluator + BlocklistCache so any in-flight reads on
 * this replica observe the prune immediately.
 */
final class CleanupExpiredManualBlocksJob implements Job
{
    public const NAME = 'cleanup-expired-manual-blocks';

    public function __construct(
        private readonly ManualBlockRepository $manualBlocks,
        private readonly AuditEmitter $audit,
        private readonly CidrEvaluatorFactory $evaluator,
        private readonly BlocklistCache $blocklistCache,
    ) {
    }

    public function name(): string
    {
        return self::NAME;
    }

    public function defaultIntervalSeconds(): int
    {
        return 86400;
    }

    public function maxRuntimeSeconds(): int
    {
        return 60;
    }

    public function run(JobContext $context): JobResult
    {
        $now = $context->clock->now();
        $expiredIds = $this->manualBlocks->findExpired($now);
        if ($expiredIds === []) {
            return new JobResult(itemsProcessed: 0);
        }

        // Snapshot labels before the rows disappear so the audit row carries
        // the IP/CIDR text rather than just an opaque numeric id.
        $labels = [];
        foreach ($expiredIds as $id) {
            $row = $this->manualBlocks->findById($id);
            $labels[$id] = $row === null
                ? null
                : ($row->kind === ManualBlock::KIND_IP
                    ? $row->ip?->text()
                    : $row->cidr?->text());
        }

        $deleted = $this->manualBlocks->deleteExpired($now);

        $auditCtx = AuditContext::system();
        foreach ($expiredIds as $id) {
            $this->audit->emit(
                AuditAction::MANUAL_BLOCK_DELETED,
                'manual_block',
                (string) $id,
                ['reason' => 'expired', 'job' => self::NAME, 'target' => $labels[$id] ?? null],
                $auditCtx,
                $labels[$id] ?? null,
            );
        }

        // Heavy-handed but correct: any in-process snapshot is invalidated
        // so subsequent reads on this replica don't keep an expired row in
        // their cached view.
        $this->evaluator->invalidate();
        $this->blocklistCache->invalidateAll();

        return new JobResult(
            itemsProcessed: $deleted,
            details: ['expired_ids' => $expiredIds],
        );
    }
}
