<?php

namespace App\Services;

use App\Models\AuditEvent;
use App\Models\Server;
use Illuminate\Support\Facades\Auth;
use phpseclib3\Crypt\EC;
use Throwable;

/**
 * Generates an SSH key, installs it, proves key login works, switches the panel's own
 * credential to it, and only THEN disables password auth — so neither the operator nor
 * the control plane can be locked out. The plaintext private key is returned ONCE for
 * the UI to reveal; it is also kept encrypted in the vault (the panel connects with it).
 */
class SshKeyProvisioner
{
    public function __construct(
        private readonly FleetService $fleet,
        private readonly HardeningService $hardening,
    ) {}

    /**
     * `privateKey` may be present even on `ok:false` — when password auth failed to disable AFTER the
     * credential was already switched to the verified key. Callers MUST display it to the operator and
     * MUST NOT log or persist it (it is the plaintext private key).
     *
     * @return array{ok: bool, privateKey?: string, publicKey?: string, error?: string}
     */
    public function enableKeyOnlyAccess(Server $server): array
    {
        $cred = $server->credential;

        if (! $cred) {
            return ['ok' => false, 'error' => __('backend.no_ssh_credential', ['server' => $server->name])];
        }

        $privateKey = null;
        $publicKey = null;

        // Only generate + switch when the panel still authenticates with a password. If the
        // credential is already a key, the operator just wants password auth turned off.
        if ($cred->auth_type !== 'key') {
            $key = EC::createKey('ed25519');
            $publicKey = $key->getPublicKey()->toString('OpenSSH', ['comment' => 'clusev-key']);
            $privateKey = (string) $key->toString('OpenSSH');

            // 1) Install the public key — additive, harmless while password auth is still on.
            try {
                $this->fleet->addAuthorizedKey($server, $publicKey);
            } catch (Throwable $e) {
                return ['ok' => false, 'error' => $e->getMessage()];
            }

            // 2) Snapshot the current credential BEFORE the switch.
            $snapshot = [
                'auth_type' => $cred->auth_type,
                'secret' => $cred->secret,
                'passphrase' => $cred->passphrase,
            ];

            // 3) Switch + verify — wrapped so a DB/SSH exception triggers best-effort restore.
            try {
                $cred->forceFill(['auth_type' => 'key', 'secret' => $privateKey, 'passphrase' => null])->save();

                $server->load('credential');
                $probe = $this->fleet->testConnection($server);
            } catch (Throwable $e) {
                // a DB/SSH error during switch+verify — best-effort restore, never half-switched.
                try {
                    $cred->forceFill($snapshot)->save();
                } catch (Throwable) {
                }

                return ['ok' => false, 'error' => $e->getMessage()];
            }

            if (! $probe['ok']) {
                $cred->forceFill($snapshot)->save();

                return ['ok' => false, 'error' => __('backend.ssh_key_verify_failed', ['error' => $probe['error'] ?? ''])];
            }

            $this->audit($server, 'ssh_key.autoprovision');
        }

        // 4) Disable password auth (the hardening guard passes now: credential is key + key installed).
        //    The credential is already the verified key, so any failure here still leaves the operator
        //    with working key access — never a lockout.
        try {
            $res = $this->hardening->apply($server, 'ssh_password', false);
        } catch (Throwable $e) {
            return array_filter([
                'ok' => false, 'error' => $e->getMessage(), 'privateKey' => $privateKey, 'publicKey' => $publicKey,
            ], fn ($v) => $v !== null);
        }

        if (! $res['ok']) {
            return array_filter([
                'ok' => false, 'error' => $res['output'], 'privateKey' => $privateKey, 'publicKey' => $publicKey,
            ], fn ($v) => $v !== null);
        }

        return array_filter([
            'ok' => true,
            'privateKey' => $privateKey,
            'publicKey' => $publicKey,
        ], fn ($v) => $v !== null);
    }

    private function audit(Server $server, string $action): void
    {
        try {
            AuditEvent::create([
                'user_id' => Auth::id(),
                'server_id' => $server->id,
                'actor' => Auth::user()?->name ?? 'system',
                'action' => $action,
                'target' => $server->name,
                'ip' => request()->ip(),
            ]);
        } catch (Throwable) {
            // auditing must never abort a verified cutover
        }
    }
}
