<?php

namespace App\Services\Proxmox;

use App\Models\Host;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use RuntimeException;

/**
 * Real Proxmox VE REST client (read-only in A: capacity + reachability). Token
 * auth over the WireGuard tunnel; the PVE certificate is self-signed so TLS
 * verification is disabled (traffic already runs inside the encrypted tunnel).
 * Not unit-tested (live I/O). The automation token itself is bootstrapped via
 * SSH `pveum` (see CreateAutomationToken), not here.
 */
class HttpProxmoxClient implements ProxmoxClient
{
    private string $baseUrl = '';

    private string $token = '';

    public function forHost(Host $host): static
    {
        $this->baseUrl = 'https://'.$host->wg_ip.':8006/api2/json';
        $this->token = (string) $host->api_token_ref;

        return $this;
    }

    private function http(): PendingRequest
    {
        return Http::withoutVerifying()
            ->withHeaders(['Authorization' => 'PVEAPIToken='.$this->token])
            ->baseUrl($this->baseUrl)
            ->timeout(15);
    }

    public function listNodes(): array
    {
        return $this->http()->get('/nodes')->throw()->json('data', []);
    }

    public function nodeStatus(string $node): array
    {
        return $this->http()->get("/nodes/{$node}/status")->throw()->json('data', []);
    }

    public function nodeStorage(string $node): array
    {
        return $this->http()->get("/nodes/{$node}/storage")->throw()->json('data', []);
    }

    public function nextVmid(): int
    {
        return (int) $this->http()->get('/cluster/nextid')->throw()->json('data');
    }

    public function cloneVm(string $node, int $templateVmid, int $newVmid, string $name): string
    {
        return (string) $this->http()->asForm()
            ->post("/nodes/{$node}/qemu/{$templateVmid}/clone", ['newid' => $newVmid, 'name' => $name, 'full' => 1])
            ->throw()->json('data');
    }

    public function setCloudInit(string $node, int $vmid, array $params): void
    {
        // PUT is the (synchronous) VM config-update operation in Proxmox.
        $this->http()->asForm()->put("/nodes/{$node}/qemu/{$vmid}/config", $params)->throw();
    }

    public function setNetworkRate(string $node, int $vmid, ?float $mbytesPerSecond): void
    {
        // The rate lives inside the net0 definition, so the existing value has
        // to be read and rewritten — sending net0=rate=… alone would drop the
        // model and bridge and detach the VM from the network.
        $config = $this->http()->get("/nodes/{$node}/qemu/{$vmid}/config")->throw()->json('data', []);
        $net0 = (string) ($config['net0'] ?? '');
        if ($net0 === '') {
            throw new RuntimeException("VM {$vmid} on {$node} has no net0 to limit.");
        }

        $parts = array_values(array_filter(
            explode(',', $net0),
            fn (string $part) => ! str_starts_with($part, 'rate='),
        ));
        if ($mbytesPerSecond !== null) {
            $parts[] = 'rate='.rtrim(rtrim(number_format($mbytesPerSecond, 3, '.', ''), '0'), '.');
        }

        $this->http()->asForm()->put("/nodes/{$node}/qemu/{$vmid}/config", ['net0' => implode(',', $parts)])->throw();
    }

    public function resizeDisk(string $node, int $vmid, string $disk, string $size): void
    {
        $this->http()->asForm()->put("/nodes/{$node}/qemu/{$vmid}/resize", ['disk' => $disk, 'size' => $size])->throw();
    }

    public function startVm(string $node, int $vmid): string
    {
        return (string) $this->http()->asForm()
            ->post("/nodes/{$node}/qemu/{$vmid}/status/start")->throw()->json('data');
    }

    public function shutdownVm(string $node, int $vmid, int $timeoutSeconds): string
    {
        // forceStop is hard-coded to 0 and is not a parameter of this method:
        // with it set, Proxmox destroys the qemu process once `timeout` expires,
        // which is a power cut to a running database. Left at 0 the task simply
        // ends in an error and the guest keeps running, which is the outcome the
        // calling step is written around.
        return (string) $this->http()->asForm()
            ->post("/nodes/{$node}/qemu/{$vmid}/status/shutdown", ['timeout' => $timeoutSeconds, 'forceStop' => 0])
            ->throw()->json('data');
    }

    public function nodeRrdData(string $node, string $timeframe = 'hour'): array
    {
        // AVERAGE, not MAX: MAX turns a single busy second into a plateau that
        // reads like sustained load. The console's question is "how busy is
        // this host", and the average is the honest answer to it.
        return $this->http()
            ->get("/nodes/{$node}/rrddata", ['timeframe' => $timeframe, 'cf' => 'AVERAGE'])
            ->throw()->json('data', []);
    }

    public function vmStatus(string $node, int $vmid): array
    {
        return $this->http()->get("/nodes/{$node}/qemu/{$vmid}/status/current")->throw()->json('data', []);
    }

    public function vmExists(string $node, int $vmid): bool
    {
        return $this->http()->get("/nodes/{$node}/qemu/{$vmid}/status/current")->successful();
    }

    /**
     * Two requests, because the wrong answer here DESTROYS something.
     *
     * BuildVmTemplate reads `false` as "no template" and starts a build whose
     * script begins with `qm destroy --purge`. So "absent" and "I could not
     * ask" must never look alike — and a status code cannot tell them apart:
     * Proxmox answers the config of a VM that does not exist with 500, the same
     * code it uses for a node in trouble.
     *
     * The VM list settles absence and nothing else: a vmid missing from a
     * successful listing is genuinely not on this node. Everything short of a
     * successful listing throws, straight into the caller's retry path, and the
     * existing template lives.
     *
     * The config document then settles the property, because that is where
     * `template` is authoritative — the list carries it too, but a field that
     * silently changed shape would make every template look like an ordinary VM
     * and rebuild it on sight.
     */
    public function isTemplate(string $node, int $vmid): bool
    {
        $vms = $this->http()->get("/nodes/{$node}/qemu")->throw()->json('data', []);

        $present = collect($vms)->contains(fn ($vm) => (int) ($vm['vmid'] ?? 0) === $vmid);

        if (! $present) {
            return false;
        }

        return (int) $this->http()->get("/nodes/{$node}/qemu/{$vmid}/config")->throw()->json('data.template', 0) === 1;
    }

    public function deleteVm(string $node, int $vmid): void
    {
        $this->http()->asForm()->delete("/nodes/{$node}/qemu/{$vmid}", ['purge' => 1])->throw();
    }

    public function guestAgentPing(string $node, int $vmid): bool
    {
        return $this->http()->asForm()->post("/nodes/{$node}/qemu/{$vmid}/agent/ping")->successful();
    }

    public function guestExec(string $node, int $vmid, string $command): array
    {
        // Wrap compound commands (cd/&&/pipes/redirects/env) in an explicit shell
        // — the agent execs a program directly. Proxmox wants the argv as REPEATED
        // `command=` fields (not indexed command[0]=…), so build the body by hand.
        $body = collect(['/bin/sh', '-c', $command])
            ->map(fn ($arg) => 'command='.rawurlencode($arg))
            ->implode('&');

        $pid = $this->http()
            ->withBody($body, 'application/x-www-form-urlencoded')
            ->post("/nodes/{$node}/qemu/{$vmid}/agent/exec")
            ->throw()->json('data.pid');

        // Poll exec-status until the command has exited.
        for ($i = 0; $i < 600; $i++) {
            $status = $this->http()
                ->get("/nodes/{$node}/qemu/{$vmid}/agent/exec-status", ['pid' => $pid])
                ->throw()->json('data', []);

            if (($status['exited'] ?? 0)) {
                return [
                    'exitcode' => (int) ($status['exitcode'] ?? 0),
                    'out-data' => (string) ($status['out-data'] ?? ''),
                ];
            }

            usleep(500000);
        }

        return ['exitcode' => 255, 'out-data' => 'guest exec timed out'];
    }

    public function taskStatus(string $node, string $upid): array
    {
        return $this->http()->get("/nodes/{$node}/tasks/{$upid}/status")->throw()->json('data', []);
    }

    public function applyFirewall(string $node, int $vmid, array $rules): void
    {
        // Clear existing rules first so a retry doesn't accumulate duplicates.
        $existing = $this->http()->get("/nodes/{$node}/qemu/{$vmid}/firewall/rules")->throw()->json('data', []);
        for ($pos = count($existing) - 1; $pos >= 0; $pos--) {
            $this->http()->delete("/nodes/{$node}/qemu/{$vmid}/firewall/rules/{$pos}")->throw();
        }

        foreach ($rules as $rule) {
            $this->http()->asForm()->post("/nodes/{$node}/qemu/{$vmid}/firewall/rules", $rule)->throw();
        }
        // Default-deny inbound so only the explicit ACCEPT rules (80/443) are open.
        $this->http()->asForm()->put("/nodes/{$node}/qemu/{$vmid}/firewall/options", [
            'enable' => 1,
            'policy_in' => 'DROP',
            'policy_out' => 'ACCEPT',
        ])->throw();
    }

    public function createBackupJob(string $node, int $vmid, string $schedule): string
    {
        // Deterministic job id → creating it twice (crash/retry) is idempotent.
        $jobId = 'clupilot-'.$vmid;

        $response = $this->http()->asForm()->post('/cluster/backup', [
            'id' => $jobId,
            'vmid' => $vmid,
            'schedule' => $schedule,
            'storage' => 'local',
            'mode' => 'snapshot',
            'enabled' => 1,
        ]);

        if ($response->failed() && ! str_contains($response->body(), 'already')) {
            $response->throw();
        }

        return $jobId;
    }
}
