<?php

declare(strict_types=1);

namespace Blax\WebRtc\Realtime;

use Blax\WebRtc\Contracts\RealtimeTransport;

/**
 * Bridges a call to OpenAI's GA Realtime API over its WebSocket transport, so the
 * SERVER — not the browser — talks to the provider. The provider is invisible to
 * the client, the model is swappable server-side, and the audio can be recorded.
 *
 * Turn-based: {@see exchange()} takes the caller's PCM utterance, drives one
 * OpenAI session (session.update → input_audio_buffer → response), and returns the
 * spoken reply as PCM. That fits both the long-lived server and a fork-per-message
 * host, where each turn runs inside a single request.
 *
 * Audio is PCM16 mono 24kHz in both directions (OpenAI's realtime wire format);
 * transcode mic audio (webm/opus, …) to that before calling and back for playback.
 *
 * GA WebSocket gotchas baked in: NO "OpenAI-Beta: realtime=v1" header (routes to
 * the retired beta), output_modalities must be ['audio'] OR ['text'] (not both),
 * audio arrives as response.output_audio.delta (base64 PCM16).
 */
final class OpenAiRealtimeBridge
{
    /** @var \Closure(string,array<string,string>,int):RealtimeTransport */
    private \Closure $transportFactory;

    public function __construct(
        ?callable $transportFactory = null,
        private string $endpoint = 'wss://api.openai.com/v1/realtime',
    ) {
        $this->transportFactory = $transportFactory !== null
            ? \Closure::fromCallable($transportFactory)
            : static fn (string $url, array $headers, int $timeout): RealtimeTransport => new TextalkRealtimeTransport($url, $headers, $timeout);
    }

    /**
     * Run one turn: send $inputPcm to OpenAI realtime and return the spoken reply.
     *
     * @param  string  $inputPcm  Raw PCM16 mono 24kHz little-endian.
     * @param  array{api_key?:string,model?:string,voice?:string,instructions?:string,timeout?:int}  $opts
     */
    public function exchange(string $inputPcm, array $opts): RealtimeTurn
    {
        $key = (string) ($opts['api_key'] ?? '');
        if ($key === '') {
            return new RealtimeTurn(error: 'missing api_key');
        }
        if ($inputPcm === '') {
            return new RealtimeTurn(error: 'empty input audio');
        }

        $model = (string) ($opts['model'] ?? 'gpt-realtime');
        $voice = (string) ($opts['voice'] ?? 'alloy');
        $instructions = (string) ($opts['instructions'] ?? '');
        $timeout = (int) ($opts['timeout'] ?? 30);

        try {
            $transport = ($this->transportFactory)(
                "{$this->endpoint}?model={$model}",
                ['Authorization' => 'Bearer '.$key],
                $timeout,
            );
        } catch (\Throwable $e) {
            return new RealtimeTurn(error: 'connect failed: '.$e->getMessage());
        }

        $pcm = '';
        $transcript = '';
        $error = null;
        $seen = [];

        try {
            // Manual push-to-talk: turn_detection null, we commit the buffer. Audio
            // in/out default to PCM16 24kHz mono, so no explicit format is needed.
            $session = [
                'type' => 'realtime',
                'audio' => [
                    'input' => ['turn_detection' => null],
                    'output' => ['voice' => $voice],
                ],
                'output_modalities' => ['audio'],
            ];
            if ($instructions !== '') {
                $session['instructions'] = $instructions;
            }
            $transport->send((string) json_encode(['type' => 'session.update', 'session' => $session]));

            // Stream the input audio in. base64 is split on 4-char boundaries
            // (65536 is a multiple of 4) so each frame stays valid.
            foreach (str_split(base64_encode($inputPcm), 65536) as $chunk) {
                $transport->send((string) json_encode(['type' => 'input_audio_buffer.append', 'audio' => $chunk]));
            }
            $transport->send((string) json_encode(['type' => 'input_audio_buffer.commit']));
            $transport->send((string) json_encode(['type' => 'response.create']));

            $deadline = time() + $timeout;
            while (time() < $deadline) {
                $raw = $transport->receive();
                if ($raw === null) {
                    break; // timeout / closed
                }
                if ($raw === '') {
                    continue;
                }

                $evt = json_decode($raw, true);
                if (! is_array($evt)) {
                    continue;
                }
                $type = (string) ($evt['type'] ?? '?');
                $seen[$type] = ($seen[$type] ?? 0) + 1;

                if (in_array($type, ['response.audio.delta', 'response.output_audio.delta'], true)) {
                    $pcm .= base64_decode((string) ($evt['delta'] ?? ''));
                } elseif (in_array($type, ['response.audio_transcript.delta', 'response.output_audio_transcript.delta'], true)) {
                    $transcript .= (string) ($evt['delta'] ?? '');
                } elseif ($type === 'error') {
                    $error = $evt['error']['message'] ?? json_encode($evt['error'] ?? $evt);
                    break;
                } elseif ($type === 'response.done') {
                    break;
                }
            }
        } catch (\Throwable $e) {
            $error = $e->getMessage();
        } finally {
            $transport->close();
        }

        return new RealtimeTurn(pcm: $pcm, transcript: trim($transcript), error: $error, events: $seen);
    }
}
