<?php

namespace App\Modules\Vm\Proxmox;

use App\Core\Vm\AbstractVm;
use Illuminate\Support\Str;

/**
 * Proxmox VE VM provider.
 *
 * Lifecycle (Hetzner pattern, PRC-37 custom_action dispatch):
 *   createAccount  → clone template + resize disk/cpu/mem + start
 *   terminateAccount → DELETE /qemu/{vmid}
 *   suspendAccount → status/stop
 *   unsuspendAccount → status/start
 *   reboot/status/console → admin custom_actions
 *
 * Auth modes — admin selects:
 *   1. api_token  (recommended; granular permission)
 *   2. password   (legacy; ticket-based)
 *
 * Spec: docs/features/VM-PROVIDER-MAPPING.md (PRC-35 mapping pattern).
 */
class Proxmox extends AbstractVm
{
    protected ProxmoxClient $client;

    /**
     * Bounded UPID task polling defaults.
     *
     * Proxmox VE'de clone/config/start/stop/delete op'ları senkron değildir:
     * bir UPID task string döner ve iş arka planda çalışır. Clone hedef VMID'de
     * "clone" LOCK tutar → task bitmeden ardışık PUT /config veya start
     * "can't lock file ... got timeout" ile patlar. Bu yüzden her async op'tan
     * sonra task bitene dek beklenir.
     */
    protected int $taskPollTimeoutSec = 300;

    protected int $taskPollIntervalSec = 2;

    public function __construct(array $config = [])
    {
        parent::__construct($config);
        $this->client = new ProxmoxTaskClient($config);
    }

    public function paramSchema(): array
    {
        return [
            ['key' => 'node',         'label' => 'PVE Node',                'required' => true],
            ['key' => 'template_id',  'label' => 'Template VM ID (clone source)', 'required' => true],
            ['key' => 'cores',        'label' => 'vCPU Cores',              'required' => true],
            ['key' => 'memory_mb',    'label' => 'Memory (MB)',             'required' => true],
            ['key' => 'disk_gb',      'label' => 'Disk size (GB)',          'required' => false],
            ['key' => 'storage',      'label' => 'PVE storage pool',        'required' => false],
            ['key' => 'bridge',       'label' => 'Network bridge',          'required' => false],
            ['key' => 'ssh_keys',     'label' => 'SSH keys (cloud-init, optional)', 'required' => false],
        ];
    }

    public function createAccount(array $params)
    {
        if (! $this->client->hasCredentials()) {
            return $this->fail('Proxmox credentials are missing — set them in module config.');
        }

        $node = $params['node'] ?? null;
        $templateId = (int) ($params['template_id'] ?? 0);
        if (! $node || ! $templateId) {
            return $this->fail('Proxmox createAccount requires node + template_id.');
        }

        // 1) Allocate next vmid
        $next = $this->client->nextVmId();
        if (! $next['ok']) {
            return $this->fail('Proxmox nextid failed: '.$next['error']);
        }
        $newId = (int) $next['data'];

        // 2) Clone template
        $hostname = $params['hostname'] ?? 'vm-'.$newId;
        $clone = $this->client->cloneVm($node, $templateId, [
            'newid' => $newId,
            'name' => $hostname,
            'full' => 1,
            'storage' => $params['storage'] ?? null,
        ]);
        if (! $clone['ok']) {
            return $this->fail('Proxmox clone failed: '.$clone['error']);
        }

        // 2b) Clone async'tir ve hedef VMID'de lock tutar — task bitene dek bekle,
        //     yoksa aşağıdaki config/start "can't lock file ... got timeout" verir.
        $cloneWait = $this->waitForTask($node, $clone['data']);
        if (! $cloneWait['ok']) {
            return $this->fail('Proxmox clone did not complete: '.$cloneWait['error']);
        }

        // 3) Resize CPU/RAM/disk (cores + memory + scsi0/disk_gb on cloned VM)
        $cfg = [
            'cores' => (int) ($params['cores'] ?? 1),
            'memory' => (int) ($params['memory_mb'] ?? 1024),
        ];
        if (! empty($params['bridge'])) {
            $cfg['net0'] = "virtio,bridge={$params['bridge']}";
        }
        $rootPassword = Str::random(20);
        // Cloud-init: ciuser/cipassword/sshkeys (if template supports cloud-init)
        $cfg['ciuser'] = 'root';
        $cfg['cipassword'] = $rootPassword;
        if (! empty($params['ssh_keys'])) {
            $cfg['sshkeys'] = rawurlencode($params['ssh_keys']);
        }
        $applyCfg = $this->client->setVmConfig($node, $newId, $cfg);
        if (! $applyCfg['ok']) {
            return $this->fail('Proxmox config apply failed: '.$applyCfg['error']);
        }
        // Config async task döndürebilir; döndürdüyse start'tan önce bitmesini bekle.
        $cfgWait = $this->waitForTask($node, $applyCfg['data']);
        if (! $cfgWait['ok']) {
            return $this->fail('Proxmox config apply did not complete: '.$cfgWait['error']);
        }

        // 4) Start
        $start = $this->client->startVm($node, $newId);
        if (! $start['ok']) {
            return $this->fail('Proxmox start failed: '.$start['error']);
        }
        $startWait = $this->waitForTask($node, $start['data']);
        if (! $startWait['ok']) {
            return $this->fail('Proxmox start did not complete: '.$startWait['error']);
        }

        return $this->ok('VM created via Proxmox', [
            'vm_id' => (string) $newId,
            'node' => $node,
            'hostname' => $hostname,
            'root_password' => $rootPassword,
            'ip_address' => null,   // Resolved by network/agent on first boot — V2 polling
        ]);
    }

    public function terminateAccount(array $params)
    {
        [$node, $vmid] = $this->resolveTarget($params);
        if (! $vmid || ! $node) {
            return $this->fail('node + vm_id required for termination');
        }
        // Stop first (PVE rejects DELETE on running/locked VM). Stop async'tir;
        // task bitene dek beklenmezse DELETE lock-timeout ile patlayabilir.
        $stop = $this->client->stopVm($node, $vmid);
        if ($stop['ok']) {
            // En iyi çaba: stop task'ini bekle; task yoksa/başarısızsa bile
            // DELETE denenir (VM zaten durmuş olabilir).
            $this->waitForTask($node, $stop['data']);
        }

        $resp = $this->client->deleteVm($node, $vmid);
        if (! $resp['ok']) {
            return $this->fail($resp['error']);
        }
        // DELETE async'tir; task'i bekleyip gerçekten silindiğini doğrula.
        $delWait = $this->waitForTask($node, $resp['data']);
        if (! $delWait['ok']) {
            return $this->fail('Proxmox delete did not complete: '.$delWait['error']);
        }

        return $this->ok('VM destroyed');
    }

    public function suspendAccount(array $params)
    {
        [$node, $vmid] = $this->resolveTarget($params);
        if (! $vmid || ! $node) {
            return $this->fail('node + vm_id required for suspend');
        }
        $resp = $this->client->stopVm($node, $vmid);

        return $resp['ok'] ? $this->ok('VM stopped') : $this->fail($resp['error']);
    }

    public function unsuspendAccount(array $params)
    {
        [$node, $vmid] = $this->resolveTarget($params);
        if (! $vmid || ! $node) {
            return $this->fail('node + vm_id required for resume');
        }
        $resp = $this->client->startVm($node, $vmid);

        return $resp['ok'] ? $this->ok('VM started') : $this->fail($resp['error']);
    }

    public function changeAccountPassword(array $params)
    {
        [$node, $vmid] = $this->resolveTarget($params);
        if (! $vmid || ! $node) {
            return $this->fail('node + vm_id required');
        }
        $newPwd = $params['new_password'] ?? Str::random(20);
        $resp = $this->client->setVmConfig($node, $vmid, ['cipassword' => $newPwd]);

        return $resp['ok']
            ? $this->ok('cloud-init password updated (effective on reboot)', ['root_password' => $newPwd])
            : $this->fail($resp['error']);
    }

    public function getAdminButtons(): array
    {
        return [
            'reboot' => ['label' => 'Reboot VM', 'icon' => 'fas fa-redo', 'class' => 'btn-outline-warning'],
            'status' => ['label' => 'Refresh Status', 'icon' => 'fas fa-sync', 'class' => 'btn-outline-info'],
            'console' => ['label' => 'Open Console', 'icon' => 'fas fa-terminal', 'class' => 'btn-outline-primary'],
        ];
    }

    public function adminCustomAction(string $action, array $params): array
    {
        return match ($action) {
            'reboot' => $this->doReboot($params),
            'status' => $this->doStatus($params),
            'console' => $this->doConsole($params),
            default => parent::adminCustomAction($action, $params),
        };
    }

    protected function doReboot(array $params): array
    {
        [$node, $vmid] = $this->resolveTarget($params);
        if (! $vmid || ! $node) {
            return $this->fail('node + vm_id required for reboot');
        }
        $resp = $this->client->rebootVm($node, $vmid);

        return $resp['ok'] ? $this->ok('VM rebooted') : $this->fail($resp['error']);
    }

    protected function doStatus(array $params): array
    {
        [$node, $vmid] = $this->resolveTarget($params);
        if (! $vmid || ! $node) {
            return $this->fail('node + vm_id required for status');
        }
        $resp = $this->client->getVmStatus($node, $vmid);
        if (! $resp['ok']) {
            return $this->fail($resp['error']);
        }
        $d = $resp['data'] ?? [];

        return $this->ok('Status fetched', [
            'power_state' => $d['status'] ?? 'unknown',
            'cpu_usage_pct' => isset($d['cpu']) ? round((float) $d['cpu'] * 100, 2) : null,
            'memory_used_mb' => isset($d['mem']) ? (int) round($d['mem'] / 1024 / 1024) : null,
            'memory_total_mb' => isset($d['maxmem']) ? (int) round($d['maxmem'] / 1024 / 1024) : null,
            'uptime_sec' => $d['uptime'] ?? null,
        ]);
    }

    protected function doConsole(array $params): array
    {
        [$node, $vmid] = $this->resolveTarget($params);
        if (! $vmid || ! $node) {
            return $this->fail('node + vm_id required for console');
        }
        $resp = $this->client->vncProxy($node, $vmid);
        if (! $resp['ok']) {
            return $this->fail($resp['error']);
        }
        // Caller can build noVNC URL from data: { ticket, port, ... } + base PVE URL
        return $this->ok('VNC ticket issued', $resp['data']);
    }

    public function testConnection(array $params)
    {
        if (! $this->client->hasCredentials()) {
            return $this->fail('Proxmox credentials missing');
        }
        $version = $this->client->getVersion();
        if (! $version['ok']) {
            return $this->fail($version['error']);
        }
        $nodes = $this->client->listNodes();
        if (! $nodes['ok']) {
            return $this->fail($nodes['error']);
        }
        $count = is_array($nodes['data']) ? count($nodes['data']) : 0;

        return $this->ok('Connection OK', [
            'version' => $version['data']['version'] ?? '?',
            'nodes_count' => $count,
        ]);
    }

    public function adminFields()
    {
        return [
            'api_url' => [
                'type' => 'url',
                'label' => 'Proxmox API URL',
                'required' => true,
                'placeholder' => 'https://pve.example.com:8006/api2/json',
                'description' => 'Full Proxmox VE API base URL (include /api2/json).',
            ],
            'auth_method' => [
                'type' => 'select',
                'label' => 'Authentication',
                'options' => ['api_token' => 'API Token (recommended)', 'password' => 'Username + Password'],
                'default' => 'api_token',
            ],
            'username' => [
                'type' => 'text',
                'label' => 'Username (password mode)',
                'placeholder' => 'root@pam',
            ],
            'password' => [
                'type' => 'password',
                'label' => 'Password (password mode)',
            ],
            'api_token_id' => [
                'type' => 'text',
                'label' => 'API Token ID (token mode)',
                'placeholder' => 'user@pam!tokenname',
            ],
            'api_token_secret' => [
                'type' => 'password',
                'label' => 'API Token Secret (token mode)',
            ],
            'verify_tls' => [
                'type' => 'boolean',
                'label' => 'Verify TLS certificate',
                'default' => true,
            ],
        ];
    }

    /**
     * @return array{0:?string, 1:?int}  [node, vmid]
     */
    protected function resolveTarget(array $params): array
    {
        $node = $params['node'] ?? $params['module_data']['node'] ?? null;
        $vmid = $params['vm_id'] ?? $params['module_data']['vm_id'] ?? null;

        return [$node ? (string) $node : null, $vmid ? (int) $vmid : null];
    }

    /**
     * Proxmox VE async UPID task'ini bitene dek bekle (bounded poll).
     *
     * PVE'de clone/config/start/stop/delete `data` alanında bir UPID task string
     * döner (örn. "UPID:node:...:qmclone:..."). İş arka planda çalışır; bu yüzden
     * ardışık op'lardan önce task'in bitmesi beklenmelidir — clone hedef VMID'de
     * lock tutar, beklenmezse sonraki PUT /config veya start
     * "can't lock file ... got timeout" verir.
     *
     * `GET /nodes/{node}/tasks/{upid}/status` `status` alanı "stopped" olana dek
     * poll edilir; bitişte `exitstatus == "OK"` başarı sayılır. Timeout aşımında
     * DÜRÜST hata döner (sahte-success yok).
     *
     * `$upid` bir UPID string değilse (senkron yanıt / null) no-op olarak başarı
     * döner — bekleyecek async iş yoktur.
     *
     * @param  mixed  $upid  PVE task UPID (op yanıtındaki `data`)
     * @return array{ok:bool, error?:string}
     */
    protected function waitForTask(string $node, $upid): array
    {
        // Async değil (ör. config senkron döndü) → bekleyecek iş yok.
        if (! is_string($upid) || ! str_starts_with($upid, 'UPID:')) {
            return ['ok' => true];
        }

        $deadline = time() + $this->taskPollTimeoutSec;
        $lastError = 'unknown task error';

        while (time() < $deadline) {
            $status = $this->client->getTaskStatus($node, $upid);
            if (! $status['ok']) {
                // Geçici okuma hatası — deadline'a kadar tekrar dene.
                $lastError = $status['error'] ?? 'task status read failed';
            } else {
                $d = is_array($status['data']) ? $status['data'] : [];
                if (($d['status'] ?? null) === 'stopped') {
                    $exit = $d['exitstatus'] ?? '';
                    if ($exit === 'OK') {
                        return ['ok' => true];
                    }

                    // Task bitti ama başarısız — PVE'nin döndürdüğü exitstatus'u aynen ver.
                    return ['ok' => false, 'error' => 'task exited: '.$exit];
                }
                // Hâlâ "running" → beklemeye devam.
            }

            sleep($this->taskPollIntervalSec);
        }

        return ['ok' => false, 'error' => 'task did not finish within '.$this->taskPollTimeoutSec.'s: '.$lastError];
    }
}

/**
 * ProxmoxClient'i async UPID task-status okuması için genişletir.
 *
 * Base client bir task-status metodu sunmaz ve `call()` protected'tir; bu
 * ince subclass korumalı transport'u yeniden kullanarak
 * `GET /nodes/{node}/tasks/{upid}/status` erişimini açar. Kimlik/token/parola
 * base client içinde kalır; burada loglama yok, ham yanıt sızdırılmaz.
 */
class ProxmoxTaskClient extends ProxmoxClient
{
    /**
     * @return array{ok:bool, data?:mixed, error?:string, status?:int}
     */
    public function getTaskStatus(string $node, string $upid): array
    {
        return $this->call('GET', "/nodes/{$node}/tasks/".rawurlencode($upid).'/status');
    }
}
