<?php

namespace App\Services;

use Exception;
use Ratchet\Client\Connector as PawlConnector;
use Ratchet\Client\WebSocket;
use Ratchet\RFC6455\Messaging\MessageInterface;
use React\EventLoop\Loop;
use Throwable;

/**
 * Short-lived OBS WebSocket v5 client used to broadcast `mpd:cmd` from the
 * Laravel side (POST /cmd/{type} and `php artisan rig:cmd`).
 *
 * Mirrors the auth + protocol opcodes implemented in public/js/obs-ws-mini.js:
 *   - op 0 Hello       → server sends auth challenge if password set
 *   - op 1 Identify    → client sends auth string + event subscriptions
 *   - op 2 Identified  → identification OK; we can now send Requests
 *   - op 6 Request     → BroadcastCustomEvent with eventData = { _type, ...payload }
 *   - op 7 Response    → request acknowledged
 *
 * Auth string: base64(SHA256(base64(SHA256(password + salt)) + challenge))
 *
 * Connect → identify → broadcast → close, all inside a single React event-loop
 * tick. Total wall time on localhost is typically 30–80 ms.
 */
class ObsWsClient
{
    private string $url;
    private string $password;
    private int $timeoutMs;

    public function __construct(?string $url = null, ?string $password = null, int $timeoutMs = 5000)
    {
        $this->url      = $url      ?? (string) config('rig.obs_ws.url');
        $this->password = $password ?? (string) config('rig.obs_ws.password');
        $this->timeoutMs = $timeoutMs;
    }

    /**
     * Broadcast a CustomEvent through OBS WS to every connected client.
     * `_type` is added to eventData so the mini client's `onCustom(typeFilter)` works.
     */
    public function broadcast(string $type, array $payload = []): void
    {
        $eventData = array_merge(['_type' => $type], $payload);
        $this->sendRequest('BroadcastCustomEvent', ['eventData' => $eventData]);
    }

    public function sendRequest(string $requestType, array $requestData = []): array
    {
        $loop = Loop::get();
        $connector = new PawlConnector($loop);

        $result      = null;
        $error       = null;
        $identified  = false;
        $requestId   = 'php-' . bin2hex(random_bytes(6));

        $promise = $connector($this->url);

        $promise->then(function (WebSocket $conn) use (&$result, &$error, &$identified, $requestId, $requestType, $requestData) {
            $conn->on('message', function (MessageInterface $msg) use ($conn, &$result, &$error, &$identified, $requestId, $requestType, $requestData) {
                $payload = json_decode((string) $msg, true);
                if (!is_array($payload) || !isset($payload['op'])) return;

                try {
                    switch ($payload['op']) {
                        case 0: // Hello
                            $auth = null;
                            if (isset($payload['d']['authentication'])) {
                                if ($this->password === '') {
                                    $error = new Exception('OBS WS server requires password but none configured');
                                    $conn->close();
                                    return;
                                }
                                $auth = $this->authString(
                                    $payload['d']['authentication']['salt'],
                                    $payload['d']['authentication']['challenge']
                                );
                            }
                            $identify = [
                                'op' => 1,
                                'd'  => [
                                    'rpcVersion'         => 1,
                                    'authentication'     => $auth,
                                    'eventSubscriptions' => 0,
                                ],
                            ];
                            // null auth would serialize as JSON null; OBS WS expects the key absent when no auth.
                            if ($auth === null) unset($identify['d']['authentication']);
                            $conn->send(json_encode($identify));
                            break;

                        case 2: // Identified
                            $identified = true;
                            $conn->send(json_encode([
                                'op' => 6,
                                'd'  => [
                                    'requestType' => $requestType,
                                    'requestId'   => $requestId,
                                    'requestData' => (object) $requestData,
                                ],
                            ]));
                            break;

                        case 7: // RequestResponse
                            if (($payload['d']['requestId'] ?? null) !== $requestId) return;
                            $status = $payload['d']['requestStatus'] ?? [];
                            if (!empty($status['result'])) {
                                $result = $payload['d']['responseData'] ?? [];
                            } else {
                                $error = new Exception(
                                    'OBS WS request failed: ' . ($status['comment'] ?? 'unknown')
                                );
                            }
                            $conn->close();
                            break;
                    }
                } catch (Throwable $e) {
                    $error = $e;
                    $conn->close();
                }
            });

            $conn->on('close', function () {
                Loop::stop();
            });
            $conn->on('error', function ($e) use (&$error) {
                $error = $e instanceof Throwable ? $e : new Exception((string) $e);
                Loop::stop();
            });
        }, function ($e) use (&$error) {
            $error = $e instanceof Throwable ? $e : new Exception((string) $e);
            Loop::stop();
        });

        // Hard timeout guard — Loop::stop fires no matter what.
        $timer = $loop->addTimer($this->timeoutMs / 1000, function () use (&$error) {
            $error = $error ?: new Exception('OBS WS request timed out');
            Loop::stop();
        });

        $loop->run();
        $loop->cancelTimer($timer);

        if ($error) throw $error;
        if (!$identified) throw new Exception('OBS WS never reached Identified');
        return $result ?? [];
    }

    private function authString(string $salt, string $challenge): string
    {
        $secret = base64_encode(hash('sha256', $this->password . $salt, true));
        return base64_encode(hash('sha256', $secret . $challenge, true));
    }
}
