<?php

namespace App\Console;

use App\Models\ProvisioningRun;
use App\Provisioning\Jobs\AdvanceRunJob;

/**
 * Scheduler tick (every minute): dispatch an advance job for every run that is
 * due. Immediate dispatch on advance handles the fast path; this catches
 * waiting/retrying runs and anything a crashed worker left behind.
 *
 * PENDING counts as left behind. A run is created and dispatched in two steps,
 * and a process that dies between them leaves a paid customer with a run that
 * nothing will ever pick up. Re-dispatching one that is merely fresh is free —
 * the runner takes a per-run lock and the second job returns immediately.
 */
class TickProvisioning
{
    /**
     * How long a run may sit pending before it counts as stranded.
     *
     * Long enough that an ordinary queue backlog is not mistaken for a crash:
     * sweeping a run whose first job is merely still queued would start a
     * second chain of continuations alongside the first, and both would keep
     * dispatching for the rest of the pipeline.
     */
    private const STRANDED_AFTER_MINUTES = 5;

    public function __invoke(): void
    {
        ProvisioningRun::query()
            ->where(function ($q) {
                $q
                    ->whereIn('status', [ProvisioningRun::STATUS_RUNNING, ProvisioningRun::STATUS_WAITING])
                    ->orWhere(fn ($stranded) => $stranded
                        ->where('status', ProvisioningRun::STATUS_PENDING)
                        ->where('created_at', '<=', now()->subMinutes(self::STRANDED_AFTER_MINUTES)));
            })
            ->where(function ($q) {
                $q->whereNull('next_attempt_at')->orWhere('next_attempt_at', '<=', now());
            })
            ->get()
            ->each(fn (ProvisioningRun $run) => AdvanceRunJob::dispatch($run->uuid));
    }
}
