<?php
/** CLI only: real handshake + ping, no messages or business writes, no secrets in output. */
if (PHP_SAPI !== 'cli') { http_response_code(404); exit; }

function chatSocketReadExact($socket, $length) {
    $value = '';
    while (strlen($value) < $length) {
        $chunk = fread($socket, $length - strlen($value));
        if ($chunk === false || $chunk === '') throw new RuntimeException('Connection closed or timed out');
        $value .= $chunk;
    }
    return $value;
}
function chatSocketProbe($service, $user, $roomId, $url) {
    $parts = parse_url($url);
    if (!$parts || !in_array($parts['scheme'] ?? '', ['ws','wss'], true) || empty($parts['host'])) throw new RuntimeException('Invalid socket URL');
    $secure = $parts['scheme'] === 'wss';
    $host = $parts['host']; $port = $parts['port'] ?? ($secure ? 443 : 80);
    $ticket = bin2hex(random_bytes(32));
    $ticketKey = $service->key('socket:'.hash('sha256',$ticket));
    $service->redis()->setex($ticketKey,30,json_encode(['roomId'=>$roomId,'userId'=>$user['id']]));
    $socket = null;
    $stage = 'TCP/TLS connection';
    try {
        $context = stream_context_create(['ssl'=>['verify_peer'=>true,'verify_peer_name'=>true,'peer_name'=>$host,'SNI_enabled'=>true]]);
        $socket = @stream_socket_client(($secure?'tls':'tcp').'://'.$host.':'.$port,$errno,$detail,8,STREAM_CLIENT_CONNECT,$context);
        if (!$socket) throw new RuntimeException('TCP/TLS connection failed (errno '.(int)$errno.')');
        stream_set_timeout($socket,8);
        $stage = 'HTTP upgrade (before 101)';
        $path = ($parts['path'] ?? '/').'?'.(!empty($parts['query'])?$parts['query'].'&':'').'ticket='.$ticket;
        $key = base64_encode(random_bytes(16));
        fwrite($socket,"GET $path HTTP/1.1\r\nHost: $host:$port\r\nOrigin: https://$host\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: $key\r\n\r\n");
        $headers = '';
        while (substr($headers,-4)!=="\r\n\r\n" && strlen($headers)<16384) $headers .= chatSocketReadExact($socket,1);
        preg_match('/^HTTP\/\S+\s+(\d+)/',$headers,$status);
        if (($status[1] ?? '')!=='101') throw new RuntimeException('Handshake HTTP '.($status[1] ?? 'unknown'));
        $accept = base64_encode(sha1($key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true));
        if (stripos($headers,'Sec-WebSocket-Accept: '.$accept."\r\n")===false) throw new RuntimeException('Invalid handshake response');
        $stage = 'heartbeat (101 received)';
        $body = '{"type":"ping"}'; $mask = random_bytes(4); $masked = '';
        for ($i=0;$i<strlen($body);$i++) $masked .= $body[$i]^$mask[$i%4];
        fwrite($socket,chr(129).chr(128|strlen($body)).$mask.$masked);
        $supportsSend = false;
        for ($i=0;$i<10;$i++) {
            $header = chatSocketReadExact($socket,2); $opcode = ord($header[0])&15; $length = ord($header[1])&127;
            if ($length===126) $length = unpack('n',chatSocketReadExact($socket,2))[1];
            if ($length===127 || $length>16384) throw new RuntimeException('Unexpected frame length');
            if ($opcode===8) throw new RuntimeException('Worker closed connection');
            $event = json_decode(chatSocketReadExact($socket,$length),true);
            if (($event['type'] ?? '')==='ready') $supportsSend = !empty($event['sendViaSocket']);
            if (($event['type'] ?? '')==='pong') return '101 + pong; WSS send '.($supportsSend?'supported':'not announced (old worker)');
        }
        throw new RuntimeException('No heartbeat response');
    } catch (RuntimeException $error) {
        $consumed = !$service->redis()->exists($ticketKey);
        throw new RuntimeException($stage.': '.$error->getMessage().'; ticket '.($consumed?'consumed by worker':'not consumed'));
    } finally {
        if (is_resource($socket)) fclose($socket);
        $service->redis()->del($ticketKey);
    }
}
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') !== __FILE__) return;
require dirname(__DIR__).'/vendor/autoload.php';
$app = new \think\App(dirname(__DIR__).'/'); $app->initialize();
$service = new \app\mobile\service\ChatService($app);
try {
    echo 'Probe namespace: '.$service->key('diagnostic').PHP_EOL;
    $user = null;
    foreach (\think\facade\Db::name('chat_member')->where(['room_id'=>1,'status'=>'joined'])->limit(50)->column('user_id') as $id) {
        try { $user = $service->user((int)$id); break; } catch (\Throwable $ignored) {}
    }
    if (!$user) throw new RuntimeException('No active public hall member available');
    $listen = $app->config->get('chat.socket_listen','');
    $internal = preg_replace('#^websocket://#','ws://',$listen);
    $public = $app->config->get('chat.socket_url','');
    $failed = false;
    foreach (['Internal WS'=>$internal,'Public WSS'=>$public] as $label=>$url) {
        try { echo '[OK] '.$label.': '.chatSocketProbe($service,$user,1,$url).PHP_EOL; }
        catch (\Throwable $error) { $failed = true; echo '[FAIL] '.$label.': '.($error instanceof RuntimeException?$error->getMessage():get_class($error)).PHP_EOL; }
    }
    exit($failed?1:0);
} catch (\Throwable $error) {
    echo '[FAIL] Initialization: '.($error instanceof RuntimeException?$error->getMessage():get_class($error)).PHP_EOL; exit(1);
}
