<?php

declare(strict_types=1);

namespace Blax\WebRtc\Signaling;

use Blax\ReactPhpKernel\Support\ConnectionRegistry;
use Blax\WebRtc\Authorization\DefaultRoomAuthorizer;
use Blax\WebRtc\Contracts\CallEventListener;
use Blax\WebRtc\Contracts\RecordingStore;
use Blax\WebRtc\Contracts\RoomAuthorizer;
use Blax\WebRtc\Events\NullCallEventListener;
use Blax\WebRtc\Rooms\RoomManager;
use Blax\WebRtc\Rooms\RoomType;
use Blax\Ws\Contracts\MessageHandler;
use Blax\Ws\Contracts\Sendable;

/**
 * The full browser signaling relay: typed rooms, presence, per-call recording and
 * an audit-log seam, over a `blax-software/laravel-ws` WebSocket.
 *
 * Rooms (type from the name prefix, like laravel-websockets channels):
 *   public · private-* (authorized) · presence-* (authorized + member list) ·
 *   open-presence-* (public + member list).
 *
 * Recording: for a peer-to-peer call the audio is DTLS-SRTP end-to-end, so the
 * server can't see it. To store the full call the browser MediaRecords its mic
 * and streams the chunks here as BINARY frames (onBinary) — the RecordingStore
 * appends them per participant and finalizes on leave/disconnect. The `record`
 * flag in `welcome`/`joined` tells the client whether to stream.
 *
 * Protocol (JSON text frames):
 *   ← welcome { peer, record }
 *   → join { room, auth?, info? }   ← joined { room, roomType, record, peers, members? }
 *                                    others ← peer-joined { room, peer, info? }
 *   → relay { to, data }            ← relay { from, data }
 *   → broadcast { data }            room ← broadcast { from, data }
 *   → leave                         ← left / others ← peer-left { peer }
 *   (binary frames) → appended to the sender's recording
 */
final class RelaySignalingHandler implements MessageHandler
{
    private ConnectionRegistry $peers;

    private RoomManager $rooms;

    private RoomAuthorizer $authorizer;

    private ?RecordingStore $recorder;

    private CallEventListener $events;

    private int $seq = 0;

    public function __construct(
        ?ConnectionRegistry $peers = null,
        ?RoomManager $rooms = null,
        ?RoomAuthorizer $authorizer = null,
        ?RecordingStore $recorder = null,
        ?CallEventListener $events = null,
    ) {
        $this->peers = $peers ?? new ConnectionRegistry;
        $this->rooms = $rooms ?? new RoomManager;
        $this->authorizer = $authorizer ?? new DefaultRoomAuthorizer;
        $this->recorder = $recorder;
        $this->events = $events ?? new NullCallEventListener;
    }

    public function peers(): ConnectionRegistry
    {
        return $this->peers;
    }

    public function rooms(): RoomManager
    {
        return $this->rooms;
    }

    public function recording(): bool
    {
        return $this->recorder !== null;
    }

    public function onOpen(Sendable $connection): void
    {
        $this->peers->add($connection->id(), $connection);
        $connection->send($this->encode([
            'type' => 'welcome',
            'peer' => $connection->id(),
            'record' => $this->recording(),
        ]));
        $this->emit(['type' => 'peer-connected', 'peer' => $connection->id()]);
    }

    public function onMessage(Sendable $connection, string $payload): void
    {
        $message = json_decode($payload, true);
        if (! is_array($message)) {
            $connection->send($this->error('invalid json'));

            return;
        }

        switch ((string) ($message['type'] ?? '')) {
            case 'join':
                $this->handleJoin($connection, $message);
                break;
            case 'leave':
                $this->departRoom($connection->id());
                $connection->send($this->encode(['type' => 'left']));
                break;
            case 'relay':
                $this->handleRelay($connection, $message);
                break;
            case 'broadcast':
                $this->handleBroadcast($connection->id(), $message);
                break;
            default:
                $connection->send($this->error('unknown type'));
        }
    }

    public function onBinary(Sendable $connection, string $payload): void
    {
        // Streamed audio for recording — append to this peer's recording in its room.
        if ($this->recorder === null) {
            return;
        }
        $room = $this->rooms->roomOf($connection->id());
        if ($room !== null) {
            $this->recorder->append($room, $connection->id(), $payload);
        }
    }

    public function onClose(Sendable $connection): void
    {
        $peer = $connection->id();
        $this->departRoom($peer);
        $this->peers->remove($peer);
        $this->emit(['type' => 'peer-disconnected', 'peer' => $peer]);
    }

    /** @param array<string,mixed> $message */
    private function handleJoin(Sendable $connection, array $message): void
    {
        $peer = $connection->id();
        $roomId = (string) ($message['room'] ?? '');
        if ($roomId === '') {
            $connection->send($this->error('missing room'));

            return;
        }

        $type = RoomType::fromName($roomId);

        if ($type->requiresAuth()) {
            $info = $this->authorizer->authorize($peer, $roomId, (array) ($message['auth'] ?? []));
            if ($info === null) {
                $connection->send($this->error('unauthorized'));

                return;
            }
        } else {
            // public keeps no info; open-presence lets the client publish its own.
            $info = $type->hasPresence() ? (array) ($message['info'] ?? []) : [];
        }

        $others = $this->rooms->join($roomId, $peer, $info);
        $room = $this->rooms->room($roomId);

        $reply = [
            'type' => 'joined',
            'room' => $roomId,
            'roomType' => $type->value,
            'record' => $this->recording(),
            'peers' => $others,
        ];
        if ($type->hasPresence() && $room !== null) {
            $reply['members'] = $room->members();
        }
        $connection->send($this->encode($reply));

        $joined = ['type' => 'peer-joined', 'room' => $roomId, 'peer' => $peer];
        if ($type->hasPresence()) {
            $joined['info'] = $info;
        }
        foreach ($others as $other) {
            $this->sendTo($other, $joined);
        }

        $this->emit(['type' => 'room-joined', 'peer' => $peer, 'room' => $roomId, 'roomType' => $type->value]);
    }

    /** Leave the current room: notify remaining peers, finalize recording, emit events. */
    private function departRoom(string $peer): void
    {
        $room = $this->rooms->roomOf($peer);
        if ($room === null) {
            return;
        }

        foreach ($this->rooms->leave($peer) as $other) {
            $this->sendTo($other, ['type' => 'peer-left', 'peer' => $peer]);
        }

        if ($this->recorder !== null) {
            $locator = $this->recorder->finalize($room, $peer);
            if ($locator !== null) {
                $this->emit(['type' => 'recording-finalized', 'peer' => $peer, 'room' => $room, 'recording' => $locator]);
            }
        }

        $this->emit(['type' => 'room-left', 'peer' => $peer, 'room' => $room]);
    }

    /** @param array<string,mixed> $message */
    private function handleRelay(Sendable $connection, array $message): void
    {
        $to = (string) ($message['to'] ?? '');
        if ($to === '') {
            $connection->send($this->error('missing to'));

            return;
        }
        if (! $this->sendTo($to, ['type' => 'relay', 'from' => $connection->id(), 'data' => $message['data'] ?? null])) {
            $connection->send($this->error("peer not connected: {$to}"));
        }
    }

    /** @param array<string,mixed> $message */
    private function handleBroadcast(string $peer, array $message): void
    {
        $room = $this->rooms->roomOf($peer);
        if ($room === null) {
            return;
        }
        foreach ($this->rooms->peersIn($room) as $other) {
            if ($other !== $peer) {
                $this->sendTo($other, ['type' => 'broadcast', 'from' => $peer, 'data' => $message['data'] ?? null]);
            }
        }
    }

    /** @param array<string,mixed> $message */
    private function sendTo(string $peerId, array $message): bool
    {
        $connection = $this->peers->get($peerId);
        if (! $connection instanceof Sendable) {
            return false;
        }

        $connection->send($this->encode($message));

        return true;
    }

    /** @param array<string,mixed> $event */
    private function emit(array $event): void
    {
        $event['seq'] = ++$this->seq;
        $this->events->handle($event);
    }

    private function error(string $error): string
    {
        return $this->encode(['type' => 'error', 'error' => $error]);
    }

    /** @param array<string,mixed> $message */
    private function encode(array $message): string
    {
        return (string) json_encode($message);
    }
}
