<?php

declare(strict_types=1);

namespace Blax\WebRtc\Media;

use Blax\WebRtc\Contracts\MediaEngine;
use Blax\WebRtc\Media\Rust\ControlClient;
use Blax\WebRtc\Media\Rust\SidecarException;
use Blax\WebRtc\Media\Rust\SidecarSupervisor;

/**
 * Production media engine: BACKEND-HOSTED rooms on the Rust SFU sidecar
 * (`rust/` in this package — str0m terminates ICE/DTLS/SRTP and forwards Opus
 * between room peers without decoding; recording is native).
 *
 * Plug-and-play: the first call ensures the sidecar is installed + running
 * (see SidecarSupervisor / BinaryManager), then speaks the JSON control
 * protocol over its Unix socket. Signaling model: only the INITIAL offer /
 * answer passes through here — renegotiation as peers join rides the WebRTC
 * data channel between browser and sidecar directly.
 *
 * The room-aware API (addPeer/removePeer/mutePeer/stats) is primary; the
 * MediaEngine contract is honored with composite "room/peer" peer ids.
 */
final class RustMediaEngine implements MediaEngine
{
    private bool $booted = false;

    public function __construct(
        private readonly SidecarSupervisor $sidecar,
        private ?ControlClient $client = null,
    ) {
        // An injected client (tests) skips sidecar boot-strapping.
        $this->booted = $client !== null;
    }

    public function name(): string
    {
        return 'rust-sfu';
    }

    // ── Room-aware primary API ─────────────────────────────────────────────

    /**
     * Add a peer to a hosted room: accepts the browser's SDP offer, returns
     * the answer. Pass $recordPath to record the peer from the first packet
     * (Ogg/Opus). Rejoining with the same ids replaces the old connection.
     */
    public function addPeer(string $room, string $peer, string $sdpOffer, ?string $recordPath = null): string
    {
        $params = ['room' => $room, 'peer' => $peer, 'offer' => $sdpOffer];
        if ($recordPath !== null) {
            $params['record'] = $recordPath;
        }

        return (string) $this->client()->request('add_peer', $params)['answer'];
    }

    public function removePeer(string $room, string $peer): void
    {
        $this->client()->request('remove_peer', ['room' => $room, 'peer' => $peer]);
    }

    /** Server-side mute: the room stops hearing the peer (recording pauses too). */
    public function mutePeer(string $room, string $peer, bool $muted): void
    {
        $this->client()->request('mute_peer', ['room' => $room, 'peer' => $peer, 'muted' => $muted]);
    }

    /**
     * Rooms/peers snapshot: connection state, mute, connected seconds (usage).
     *
     * @return list<array{room:string,peers:list<array{peer:string,connected:bool,muted:bool,seconds:float}>}>
     */
    public function stats(): array
    {
        return (array) ($this->client()->request('stats')['rooms'] ?? []);
    }

    /**
     * Buffered sidecar notices (peer_connected / peer_left with usage seconds).
     *
     * @return list<array<string,mixed>>
     */
    public function drainEvents(): array
    {
        return $this->client()->drainEvents();
    }

    // ── MediaEngine contract (peer id = "room/peer") ───────────────────────

    public function offer(string $peerId, string $sdpOffer): string
    {
        [$room, $peer] = $this->split($peerId);

        return $this->addPeer($room, $peer, $sdpOffer);
    }

    public function addIceCandidate(string $peerId, array $candidate): void
    {
        // Nothing to forward: the answer carries the sidecar's host candidate,
        // and str0m (full ICE) learns the browser's candidates from the STUN
        // binding requests arriving on the media socket.
    }

    public function startRecording(string $peerId, string $path): void
    {
        [$room, $peer] = $this->split($peerId);
        $this->client()->request('record_start', ['room' => $room, 'peer' => $peer, 'path' => $path]);
    }

    public function stopRecording(string $peerId): void
    {
        [$room, $peer] = $this->split($peerId);
        $this->client()->request('record_stop', ['room' => $room, 'peer' => $peer]);
    }

    public function connectPeers(string $peerA, string $peerB): void
    {
        // Nothing to do: the SFU forwards between all peers of a room
        // automatically — room membership IS the routing table.
    }

    public function bridge(string $peerId, array $options): void
    {
        throw new SidecarException(
            'AI bridging inside the Rust data plane is not implemented yet — '.
            'bridge via Realtime\OpenAiRealtimeBridge for now (duplex in-SFU bridge is #1051).'
        );
    }

    public function close(string $peerId): void
    {
        [$room, $peer] = $this->split($peerId);
        $this->removePeer($room, $peer);
    }

    // ───────────────────────────────────────────────────────────────────────

    /** @return array{0:string,1:string} */
    private function split(string $peerId): array
    {
        $pos = strpos($peerId, '/');
        if ($pos === false) {
            return ['default', $peerId];
        }

        return [substr($peerId, 0, $pos), substr($peerId, $pos + 1)];
    }

    private function client(): ControlClient
    {
        if (! $this->booted) {
            $this->sidecar->ensureRunning();
            $this->booted = true;
        }

        return $this->client ??= new ControlClient($this->sidecar->socketPath());
    }
}
