<?php

namespace App\Services\Provisioning;

use App\Models\Instance;
use App\Models\InstanceMetric;
use App\Services\Proxmox\ProxmoxClient;

/**
 * How full a customer's cloud actually is, read from inside it.
 *
 * Proxmox's own `disk` figure for a VM is the allocated image, not what is used
 * within it — it would show a customer 500 GB from the first day. `df` in the
 * guest is the number they recognise, and it is the number the downgrade check
 * blocks on, so there must be exactly one way of taking it.
 *
 * There are two callers and they want it for opposite reasons.
 * CollectInstanceTraffic takes it on its rounds, for the trend and for the
 * nightly picture. The portal takes it ON DEMAND, because a customer who has
 * just deleted forty gigabytes to fit into a smaller package must not be told to
 * come back tomorrow — the whole value of the answer is that it is current.
 * Both go through ProxmoxClient, so a test fakes it and nothing needs a real
 * guest.
 */
final class DiskUsageProbe
{
    public function __construct(private ProxmoxClient $pve) {}

    /**
     * A reading, or null when the guest could not give one.
     *
     * Null is not zero, and the difference matters everywhere this is used: a
     * failed read charted as 0 GB tells a customer their data is gone, and a
     * failed read taken as "empty" would wave a downgrade through that is not
     * actually possible.
     *
     * @return array{used: int, total: int}|null
     */
    public function read(Instance $instance): ?array
    {
        if ($instance->host === null || $instance->vmid === null) {
            return null;
        }

        $node = $instance->host->node ?? 'pve';
        $client = $this->pve->forHost($instance->host);

        if (! $client->guestAgentPing($node, (int) $instance->vmid)) {
            return null;
        }

        // -B1 so the numbers are bytes and no locale can reinterpret them.
        $result = $client->guestExec($node, (int) $instance->vmid,
            'df -B1 --output=size,used /var/www 2>/dev/null || df -B1 --output=size,used /');
        $out = trim((string) ($result['out-data'] ?? $result['output'] ?? ''));

        // Header line, then one line of two integers. Anything else is a shell
        // that answered something we did not ask for, and is discarded rather
        // than parsed hopefully.
        $lines = preg_split('/\r?\n/', $out) ?: [];
        $last = trim((string) end($lines));

        if (preg_match('/^(\d+)\s+(\d+)$/', $last, $m) !== 1) {
            return null;
        }

        return ['total' => (int) $m[1], 'used' => (int) $m[2]];
    }

    /**
     * Take a reading and write it into today's row, so everything that reads the
     * fill level sees it.
     *
     * The same row the sampler accumulates into, and the fill level is
     * overwritten rather than added to — it is a state, not a flow. Returns null
     * when there was nothing to record, so a caller can say "we could not look"
     * instead of "you are fine".
     */
    public function record(Instance $instance): ?InstanceMetric
    {
        $disk = $this->read($instance);

        if ($disk === null) {
            return null;
        }

        $metric = InstanceMetric::query()->firstOrCreate(
            ['instance_id' => $instance->id, 'day' => now()->toDateString()],
        );

        $metric->disk_used_bytes = $disk['used'];
        $metric->disk_total_bytes = $disk['total'];
        $metric->save();

        return $metric;
    }
}
