<?php

declare(strict_types=1);

namespace PhpWA\Transport;

use PhpWA\Exception\TransportException;
use PhpWA\Runtime\Deadline;

final class TlsStreamEnabler
{
    /** @param resource $stream */
    public static function enable($stream, string $peerName, Deadline $deadline): void
    {
        if (!is_resource($stream) || trim($peerName) === '') {
            throw new TransportException('TLS stream or peer name is invalid.');
        }
        stream_context_set_option($stream, 'ssl', 'SNI_enabled', true);
        stream_context_set_option($stream, 'ssl', 'peer_name', $peerName);
        stream_context_set_option($stream, 'ssl', 'verify_peer', true);
        stream_context_set_option($stream, 'ssl', 'verify_peer_name', true);
        stream_context_set_option($stream, 'ssl', 'allow_self_signed', false);
        stream_context_set_option($stream, 'ssl', 'disable_compression', true);

        if (!stream_set_blocking($stream, false)) {
            throw new TransportException('Unable to prepare non-blocking TLS handshake.');
        }
        try {
            while (true) {
                set_error_handler(static function (): bool { return true; });
                try {
                    $enabled = stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
                } finally {
                    restore_error_handler();
                }
                if ($enabled === true) { return; }
                if ($enabled === false) { throw new TransportException('TLS handshake failed.'); }

                $remaining = $deadline->remaining('TLS handshake');
                $read = [$stream];
                $write = [$stream];
                $except = [];
                $seconds = (int) floor($remaining);
                $microseconds = (int) max(0, ($remaining - $seconds) * 1000000);
                set_error_handler(static function (): bool { return true; });
                try {
                    $ready = stream_select($read, $write, $except, $seconds, $microseconds);
                } finally {
                    restore_error_handler();
                }
                if ($ready === false) { throw new TransportException('Failed while waiting for TLS handshake.'); }
                if ($ready === 0) { $deadline->remaining('TLS handshake'); }
                if ($read === []) { usleep((int) min(10000, max(1000, $remaining * 1000000))); }
            }
        } finally {
            if (is_resource($stream)) { stream_set_blocking($stream, true); }
        }
    }

    private function __construct()
    {
    }
}
