<?php

namespace App\Provisioning;

use App\Models\Order;
use App\Models\ProvisioningRun;
use InvalidArgumentException;

/**
 * Is the work somebody is about to start already going to happen anyway?
 *
 * Every customer pipeline shares one subject — the instance's own purchase Order
 * — so an action that wants to start a run has to look at what is already in
 * flight against it. Two runs writing one router, or resizing one disk from two
 * different figures, is the failure this guards.
 *
 * It used to ask whether ANYTHING was in flight, and stand aside if it was, on
 * the reasoning that whatever is running reads the current state when it gets
 * there. That is true only of a run that actually contains the steps in question.
 * It was false for `restart`, which has no address steps, and for `address`, which
 * has no storage steps — with two consequences that both cost the customer
 * something real:
 *
 *  - a domain proven while a restart was running was never routed, and nothing
 *    looked again: the nightly proof check only re-applies an address on the
 *    FLIP from unproven to proven (see VerifyCustomDomains), and that flip had
 *    already happened.
 *  - a storage pack booked while an address run was going was charged every
 *    month and never delivered, because nothing else ever grows a disk.
 *
 * So the question is now what it always meant: will the run in flight carry out
 * this work? Where the answer is no, the new run starts beside it. That is safe
 * for the pairs it allows, and only for those: the caller has already established
 * that the machine is LIVE (Instance::hasLiveMachine()), which rules out standing
 * beside the build; a run whose pipeline covers the work still wins; and what is
 * left writes different things — a router file and a DNS record on one side, a
 * disk and a filesystem on the other, with Nextcloud's config.php and its
 * appconfig table apart from each other. The one genuinely awkward pair is a
 * `restart`, where the guest goes away underneath the newcomer; those steps throw
 * on an unreachable guest, which the runner turns into a retry, and the backoff
 * outlasts a reboot.
 */
class WorkInFlight
{
    public function __construct(private PipelineRegistry $registry) {}

    /**
     * Will a run already going against this order carry out every step of
     * `$pipeline`?
     */
    public function covers(Order $order, string $pipeline): bool
    {
        $runs = ProvisioningRun::query()
            ->where('subject_type', Order::class)
            ->where('subject_id', $order->id)
            ->inFlight()
            ->get(['pipeline', 'current_step']);

        foreach ($runs as $run) {
            try {
                if ($this->registry->stillCovers($run->pipeline, $run->current_step, $pipeline)) {
                    return true;
                }
            } catch (InvalidArgumentException) {
                // A pipeline that no longer exists (renamed in a deploy while a
                // run was in flight) covers nothing. The run will fail at its
                // next advance for the same reason; refusing to start the work
                // over it would lose the work as well.
                continue;
            }
        }

        return false;
    }
}
