<?php

declare(strict_types=1);

namespace Apexl\Io\Jobs\Services;

use Apexl\Config\ConfigurationInterface;
use Apexl\Io\Jobs\Interfaces\DeferredTaskExceptionInterface;
use Apexl\Io\Jobs\Interfaces\QueuedTaskFinderInterface;
use Throwable;

final readonly class TaskRunnerService
{
    public function __construct(
        private QueuedTaskFinderInterface $queuedTaskFinder,
        private ConfigurationInterface $config,
    ) {}

    public function run(?int $limit = 10): void
    {
        $batches = $this->queuedTaskFinder->tasks($limit);
        \Apexl\Io\Logger\logger('queue')->debug('Starting tasks');
        foreach ($batches as $ix => $tasks) {
            \Apexl\Io\Logger\logger('queue')->debug('Starting batch {batch}...', [
                'batch' => $ix,
            ]);
            foreach ($tasks as $task) {
                try {
                    if ($task->isLocked()) {
                        return;
                    }

                    // Asked before locking, so declining is one write rather than four —
                    // lock, attempts, run_after, unlock — plus the error line that a
                    // deliberate stand-down used to produce. `continue`, not `return`: a
                    // handler holding its own jobs back has no claim on anyone else's, and
                    // the batch is ordered across handlers.
                    if (!$task->shouldRun()) {
                        \Apexl\Io\Logger\logger('queue')->debug('Skipping {task}: the handler is not ready', [
                            'task' => $task->name(),
                        ]);

                        $task->defer();

                        continue;
                    }

                    $task->lock();
                    \Apexl\Io\Logger\logger('queue')->info('Starting {task}', [
                        'task' => $task->name(),
                    ]);
                    $task->run();
                    \Apexl\Io\Logger\logger('queue')->info('Completed {task}', [
                        'task' => $task->name(),
                    ]);
                    if ($this->config->get('jobs.retain_processed_jobs')) {
                        $task->markProcessed();
                        $task->unlock();
                    } else {
                        $task->delete();
                    }
                } catch (Throwable $throwable) {
                    // A handler with no way to say "not now" except by throwing was having
                    // every deliberate deferral logged as an error. Nothing about failure
                    // handling changes here — handleFailure() still decides whether the job
                    // is deferred or failed — only whether anyone is told it went wrong.
                    $context = [
                        'task' => $task->name(),
                        'error' => $throwable->getMessage(),
                        'exception' => $throwable,
                    ];

                    if ($throwable instanceof DeferredTaskExceptionInterface) {
                        \Apexl\Io\Logger\logger('queue')->debug('Deferred {task}: {error}', $context);
                    } else {
                        \Apexl\Io\Logger\logger('queue')->error('Error running {task}: {error}', $context);
                    }

                    $task->handleFailure($task->name(), $throwable);
                    $task->unlock();
                }
            }
            \Apexl\Io\Logger\logger('queue')->debug('Completed batch {batch}...', [
                'batch' => $ix,
            ]);
        }
        \Apexl\Io\Logger\logger('queue')->debug('Completed all requested tasks');
    }
}
