<?php

if (!defined('ABSPATH')) {
    exit;
}

final class SPP_Http_Proxmox_Client implements SPP_Proxmox_Client
{
    /** @var array{base_url:string, token_id:string, token_secret:string, node:string, lxc_rootfs_storage:string, lxc_bridge:string} */
    private array $options;

    /**
     * @param array{base_url:string, token_id:string, token_secret:string, node:string, lxc_rootfs_storage?:string, lxc_bridge?:string} $options
     */
    public function __construct(array $options)
    {
        $this->options = array_merge([
            'lxc_rootfs_storage' => '',
            'lxc_bridge' => 'vmbr0',
        ], $options);
    }

    public function list_templates(): array
    {
        $templates = array_merge(
            $this->list_qemu_templates(),
            $this->list_lxc_templates()
        );

        usort(
            $templates,
            static function (array $left, array $right): int {
                $type_compare = strcmp((string) $left['provisioningType'], (string) $right['provisioningType']);

                return $type_compare !== 0
                    ? $type_compare
                    : strcasecmp((string) $left['name'], (string) $right['name']);
            }
        );

        return $templates;
    }

    public function provision_instance(array $input): array
    {
        return $this->normalise_type((string) ($input['provisioning_type'] ?? 'qemu')) === 'lxc'
            ? $this->create_lxc_container($input)
            : $this->clone_qemu_vm($input);
    }

    public function start_instance(string $type, int $vm_id): void
    {
        $this->request($this->guest_path($type, $vm_id) . '/status/start', 'POST');
    }

    public function stop_instance(string $type, int $vm_id): void
    {
        $this->request($this->guest_path($type, $vm_id) . '/status/stop', 'POST');
    }

    public function delete_instance(string $type, int $vm_id): void
    {
        $this->request($this->guest_path($type, $vm_id), 'DELETE');
    }

    public function get_status(string $type, int $vm_id): string
    {
        $data = $this->request($this->guest_path($type, $vm_id) . '/status/current', 'GET');

        return is_array($data) && isset($data['status']) ? (string) $data['status'] : 'unknown';
    }

    public function get_ip_addresses(string $type, int $vm_id): array
    {
        return $this->normalise_type($type) === 'lxc'
            ? $this->get_lxc_ip_addresses($vm_id)
            : $this->get_qemu_ip_addresses($vm_id);
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function list_qemu_templates(): array
    {
        $data = $this->request("/nodes/{$this->options['node']}/qemu", 'GET');
        $vms = is_array($data) ? $data : [];
        $templates = [];

        foreach ($vms as $vm) {
            if (!is_array($vm) || empty($vm['template']) || empty($vm['vmid'])) {
                continue;
            }

            $templates[] = [
                'provisioningType' => 'qemu',
                'vmId' => (int) $vm['vmid'],
                'templateRef' => '',
                'storage' => '',
                'name' => isset($vm['name']) && $vm['name'] !== '' ? (string) $vm['name'] : 'template-' . (int) $vm['vmid'],
                'cpuCores' => max(1, (int) ($vm['cpus'] ?? 1)),
                'memoryMb' => $this->bytes_to_mb((int) ($vm['maxmem'] ?? 0)),
                'diskGb' => $this->bytes_to_gb((int) ($vm['maxdisk'] ?? 0)),
                'status' => isset($vm['status']) ? (string) $vm['status'] : 'unknown',
            ];
        }

        return $templates;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function list_lxc_templates(): array
    {
        try {
            $storages = $this->request("/nodes/{$this->options['node']}/storage", 'GET');
        } catch (Throwable) {
            return [];
        }

        $storages = is_array($storages) ? $storages : [];
        $templates = [];

        foreach ($storages as $storage) {
            if (!is_array($storage) || empty($storage['storage'])) {
                continue;
            }

            if (isset($storage['enabled']) && (int) $storage['enabled'] !== 1) {
                continue;
            }

            $content = is_array($storage['content'] ?? null)
                ? implode(',', $storage['content'])
                : (string) ($storage['content'] ?? '');

            if ($content !== '' && !str_contains(strtolower($content), 'vztmpl')) {
                continue;
            }

            $storage_id = (string) $storage['storage'];

            try {
                $items = $this->request(
                    add_query_arg(['content' => 'vztmpl'], "/nodes/{$this->options['node']}/storage/" . rawurlencode($storage_id) . '/content'),
                    'GET'
                );
            } catch (Throwable) {
                continue;
            }

            foreach (is_array($items) ? $items : [] as $item) {
                if (!is_array($item)) {
                    continue;
                }

                $volid = (string) ($item['volid'] ?? $item['volume'] ?? '');
                if ($volid === '' || (isset($item['content']) && (string) $item['content'] !== 'vztmpl')) {
                    continue;
                }

                $templates[] = [
                    'provisioningType' => 'lxc',
                    'vmId' => 0,
                    'templateRef' => $volid,
                    'storage' => $storage_id,
                    'name' => $this->template_name_from_volid($volid),
                    'cpuCores' => 1,
                    'memoryMb' => 1024,
                    'diskGb' => 8,
                    'status' => 'available',
                ];
            }
        }

        return $templates;
    }

    /**
     * @param array<string, mixed> $input
     * @return array{vm_id:int}
     */
    private function clone_qemu_vm(array $input): array
    {
        $vm_id = (int) $this->request('/cluster/nextid', 'GET');
        $template_id = (int) $input['template_vm_id'];

        $this->request("/nodes/{$this->options['node']}/qemu/{$template_id}/clone", 'POST', [
            'newid' => $vm_id,
            'name' => (string) $input['name'],
            'full' => 1,
        ]);

        $this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/config", 'PUT', [
            'cores' => (int) $input['cpu_cores'],
            'memory' => (int) $input['memory_mb'],
            'tags' => $this->tags_string($input['tags'] ?? []),
        ]);

        return ['vm_id' => $vm_id];
    }

    /**
     * @param array<string, mixed> $input
     * @return array{vm_id:int}
     */
    private function create_lxc_container(array $input): array
    {
        $rootfs_storage = $this->sanitize_proxmox_identifier((string) $this->options['lxc_rootfs_storage']);
        if ($rootfs_storage === '') {
            throw new RuntimeException('Missing LXC rootfs storage configuration.');
        }

        $vm_id = (int) $this->request('/cluster/nextid', 'GET');
        $body = [
            'vmid' => $vm_id,
            'hostname' => $this->sanitize_hostname((string) $input['name']),
            'ostemplate' => (string) $input['lxc_template_ref'],
            'cores' => (int) $input['cpu_cores'],
            'memory' => (int) $input['memory_mb'],
            'rootfs' => $rootfs_storage . ':' . max(1, (int) $input['disk_gb']),
            'unprivileged' => 1,
            'tags' => $this->tags_string($input['tags'] ?? []),
        ];

        $bridge = $this->sanitize_proxmox_identifier((string) $this->options['lxc_bridge']);
        if ($bridge !== '') {
            $body['net0'] = 'name=eth0,bridge=' . $bridge . ',ip=dhcp';
        }

        $this->request("/nodes/{$this->options['node']}/lxc", 'POST', $body);

        return ['vm_id' => $vm_id];
    }

    private function get_qemu_ip_addresses(int $vm_id): array
    {
        $data = $this->request("/nodes/{$this->options['node']}/qemu/{$vm_id}/agent/network-get-interfaces", 'GET');
        $interfaces = is_array($data) && isset($data['result']) && is_array($data['result']) ? $data['result'] : [];
        $ips = [];

        foreach ($interfaces as $interface) {
            if (!is_array($interface) || empty($interface['ip-addresses']) || !is_array($interface['ip-addresses'])) {
                continue;
            }

            foreach ($interface['ip-addresses'] as $address) {
                if (!is_array($address) || empty($address['ip-address'])) {
                    continue;
                }

                $ip = (string) $address['ip-address'];
                if ($ip === '127.0.0.1' || $ip === '::1' || str_starts_with($ip, 'fe80:')) {
                    continue;
                }

                $ips[] = $ip;
            }
        }

        return array_values(array_unique($ips));
    }

    private function get_lxc_ip_addresses(int $vm_id): array
    {
        $data = $this->request("/nodes/{$this->options['node']}/lxc/{$vm_id}/interfaces", 'GET');
        $interfaces = is_array($data) ? $data : [];
        $ips = [];

        foreach ($interfaces as $interface) {
            if (!is_array($interface)) {
                continue;
            }

            foreach (['inet', 'inet6'] as $key) {
                if (empty($interface[$key])) {
                    continue;
                }

                $ip = preg_replace('/\/\d+$/', '', (string) $interface[$key]);
                if ($ip === null || $ip === '127.0.0.1' || $ip === '::1' || str_starts_with($ip, 'fe80:')) {
                    continue;
                }

                $ips[] = $ip;
            }
        }

        return array_values(array_unique($ips));
    }

    private function guest_path(string $type, int $vm_id): string
    {
        $guest_type = $this->normalise_type($type) === 'lxc' ? 'lxc' : 'qemu';

        return "/nodes/{$this->options['node']}/{$guest_type}/{$vm_id}";
    }

    private function normalise_type(string $type): string
    {
        return strtolower($type) === 'lxc' ? 'lxc' : 'qemu';
    }

    /**
     * @param mixed $tags
     */
    private function tags_string($tags): string
    {
        if (!is_array($tags)) {
            return '';
        }

        $clean = [];
        foreach ($tags as $tag) {
            $tag = strtolower(sanitize_title((string) $tag));
            if ($tag !== '') {
                $clean[] = substr($tag, 0, 40);
            }
        }

        return implode(';', array_values(array_unique($clean)));
    }

    private function sanitize_hostname(string $name): string
    {
        $hostname = strtolower(sanitize_title($name));
        $hostname = preg_replace('/[^a-z0-9-]/', '-', $hostname);
        $hostname = trim((string) $hostname, '-');

        return substr($hostname !== '' ? $hostname : 'spp-lxc', 0, 63);
    }

    private function sanitize_proxmox_identifier(string $value): string
    {
        $value = sanitize_text_field($value);
        $value = preg_replace('/[^A-Za-z0-9_.-]/', '', $value);

        return $value === null ? '' : $value;
    }

    private function template_name_from_volid(string $volid): string
    {
        $name = basename(str_replace('\\', '/', $volid));
        $name = preg_replace('/\.(tar\.zst|tar\.xz|tar\.gz|tar)$/', '', $name);

        return $name !== null && $name !== '' ? $name : $volid;
    }

    private function bytes_to_mb(int $bytes): int
    {
        if ($bytes < 1) {
            return 1024;
        }

        return max(128, (int) ceil($bytes / 1048576));
    }

    private function bytes_to_gb(int $bytes): int
    {
        if ($bytes < 1) {
            return 8;
        }

        return max(1, (int) ceil($bytes / 1073741824));
    }

    /**
     * @param array<string, int|string> $body
     * @return mixed
     */
    private function request(string $path, string $method, array $body = [])
    {
        foreach (['base_url', 'token_id', 'token_secret', 'node'] as $key) {
            if ($this->options[$key] === '') {
                throw new RuntimeException('Missing Proxmox HTTP configuration.');
            }
        }

        $url = trailingslashit($this->options['base_url']) . 'api2/json' . $path;
        $response = wp_remote_request($url, [
            'method' => $method,
            'timeout' => 30,
            'headers' => [
                'Authorization' => 'PVEAPIToken=' . $this->options['token_id'] . '=' . $this->options['token_secret'],
            ],
            'body' => $body,
        ]);

        if (is_wp_error($response)) {
            throw new RuntimeException($response->get_error_message());
        }

        $status = (int) wp_remote_retrieve_response_code($response);
        if ($status < 200 || $status >= 300) {
            throw new RuntimeException('Proxmox request failed with HTTP ' . $status);
        }

        $payload = json_decode(wp_remote_retrieve_body($response), true);

        return is_array($payload) && array_key_exists('data', $payload) ? $payload['data'] : null;
    }
}
