<?php

namespace App\Services\Ssh;

use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Net\SSH2;
use RuntimeException;

/**
 * Real SSH session via phpseclib3. Not unit-tested (live I/O); verified against
 * a real host during end-to-end runs. Files are written via a base64 heredoc so
 * a single SSH channel suffices (no separate SFTP subsystem needed).
 */
class PhpseclibRemoteShell implements RemoteShell
{
    private ?SSH2 $ssh = null;

    public function connectWithPassword(string $host, string $user, string $password): void
    {
        $ssh = new SSH2($host);
        $ssh->setTimeout($this->commandTimeout());

        if (! $ssh->login($user, $password)) {
            throw new RuntimeException("SSH password login failed for {$user}@{$host}");
        }

        $this->ssh = $ssh;
    }

    public function connectWithKey(string $host, string $user, string $privateKey, ?string $expectedFingerprint = null): void
    {
        $key = PublicKeyLoader::load($privateKey);
        $ssh = new SSH2($host);
        $ssh->setTimeout($this->commandTimeout());

        if (! $ssh->login($user, $key)) {
            throw new RuntimeException("SSH key login failed for {$user}@{$host}");
        }

        $this->ssh = $ssh;

        // Host-key pinning: reject a server whose key changed since onboarding.
        if (filled($expectedFingerprint) && $this->hostKeyFingerprint() !== $expectedFingerprint) {
            $this->ssh = null;
            throw new RuntimeException("SSH host key mismatch for {$host} (possible interception or re-provision).");
        }
    }

    public function run(string $command): CommandResult
    {
        $stdout = (string) $this->ssh()->exec($command);
        $exit = $this->ssh()->getExitStatus();

        // No exit status = interrupted/closed channel → treat as failure, never success.
        return new CommandResult(is_int($exit) ? $exit : 255, $stdout, '');
    }

    public function putFile(string $remotePath, string $contents): void
    {
        $b64 = base64_encode($contents);
        $dir = escapeshellarg(dirname($remotePath));
        $path = escapeshellarg($remotePath);

        $result = $this->run("mkdir -p {$dir} && printf %s ".escapeshellarg($b64)." | base64 -d > {$path}");

        if (! $result->ok()) {
            throw new RuntimeException("Failed to write remote file {$remotePath}");
        }
    }

    public function hostKeyFingerprint(): string
    {
        $key = $this->ssh()->getServerPublicHostKey();

        return $key === false ? '' : 'SHA256:'.base64_encode(hash('sha256', (string) $key, true));
    }

    /** Long provisioning commands (apt full-upgrade …) far exceed the ~10s default. */
    private function commandTimeout(): int
    {
        return (int) config('provisioning.ssh.command_timeout', 2000);
    }

    private function ssh(): SSH2
    {
        if ($this->ssh === null) {
            throw new RuntimeException('RemoteShell is not connected.');
        }

        return $this->ssh;
    }
}
