<?php

declare(strict_types=1);

namespace Blax\WebRtc\Signaling;

use Blax\WebRtc\Contracts\MediaEngine;
use Blax\WebRtc\Rooms\RoomManager;

/**
 * Transport-agnostic signaling router: decodes a signaling message and drives
 * the MediaEngine, returning the reply to send back (or null when a message
 * needs no reply, e.g. trickle ICE).
 *
 * Kept free of any socket/WS concern so it can be exercised in unit tests and so
 * the browser-facing transport (this package's raw SignalingServer today, or the
 * laravel-websockets WS layer later) is a swappable detail.
 *
 * Wire shape (JSON): { "type": "offer|ice|record|connect|bridge|close|join|leave", "peer": "...", ... }
 */
final class SignalingHandler
{
    private RoomManager $rooms;

    public function __construct(private MediaEngine $engine, ?RoomManager $rooms = null)
    {
        $this->rooms = $rooms ?? new RoomManager;
    }

    /** The shared room registry (party-to-party / group calls). */
    public function rooms(): RoomManager
    {
        return $this->rooms;
    }

    /**
     * @param  array<string,mixed>  $message
     * @return array<string,mixed>|null
     */
    public function handle(array $message): ?array
    {
        $type = (string) ($message['type'] ?? '');
        $peer = (string) ($message['peer'] ?? '');

        if ($peer === '') {
            return ['type' => 'error', 'error' => 'missing peer'];
        }

        try {
            return match ($type) {
                'offer' => [
                    'type' => 'answer',
                    'peer' => $peer,
                    'sdp' => $this->engine->offer($peer, (string) ($message['sdp'] ?? '')),
                ],
                'ice' => $this->trickle($peer, (array) ($message['candidate'] ?? [])),
                'record' => $this->ack($peer, fn () => $this->engine->startRecording($peer, (string) ($message['path'] ?? ''))),
                'connect' => $this->ack($peer, fn () => $this->engine->connectPeers($peer, (string) ($message['other'] ?? ''))),
                'bridge' => $this->ack($peer, fn () => $this->engine->bridge($peer, (array) ($message['options'] ?? []))),
                'close' => $this->close($peer),
                'join' => $this->join($peer, (string) ($message['room'] ?? '')),
                'leave' => $this->leave($peer),
                default => ['type' => 'error', 'error' => "unknown type: {$type}"],
            };
        } catch (\Throwable $e) {
            return ['type' => 'error', 'peer' => $peer, 'error' => $e->getMessage()];
        }
    }

    /** @param array<string,mixed> $candidate */
    private function trickle(string $peer, array $candidate): ?array
    {
        $this->engine->addIceCandidate($peer, $candidate);

        return null; // trickle ICE needs no reply
    }

    /** @return array<string,mixed> */
    private function ack(string $peer, callable $action): array
    {
        $action();

        return ['type' => 'ack', 'peer' => $peer];
    }

    /** Tear a peer down: drop it from its room, then close its media. */
    private function close(string $peer): array
    {
        $this->rooms->leave($peer);
        $this->engine->close($peer);

        return ['type' => 'ack', 'peer' => $peer];
    }

    /**
     * Join a room and mesh with everyone already there.
     *
     * @return array<string,mixed>
     */
    private function join(string $peer, string $roomId): array
    {
        if ($roomId === '') {
            return ['type' => 'error', 'peer' => $peer, 'error' => 'missing room'];
        }

        $others = $this->rooms->join($roomId, $peer);
        foreach ($others as $other) {
            $this->engine->connectPeers($peer, $other);
        }

        return ['type' => 'joined', 'peer' => $peer, 'room' => $roomId, 'peers' => $others];
    }

    /**
     * Leave the current room; the reply lists the peers that remain.
     *
     * @return array<string,mixed>
     */
    private function leave(string $peer): array
    {
        $remaining = $this->rooms->leave($peer);

        return ['type' => 'left', 'peer' => $peer, 'peers' => $remaining];
    }
}
