<?php

namespace App\Events;

use App\Services\Settings;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

/**
 * Broadcast a screen save to a project's live editors (Team Workspaces P6 /
 * Tier 2 — Reverb opt-in). Peers apply the change via the editor's existing
 * clean-refresh path.
 *
 * This event ONLY broadcasts when the operator has enabled the Reverb tier
 * (`collab.transport === 'reverb'`) — `broadcastWhen()` gates it so a default
 * (`lock`) install never attempts a websocket emit even if a stray
 * `broadcast()` call is wired. The controller additionally only constructs
 * the event when transport=reverb, so this is belt-and-suspenders. Under the
 * default lock transport the event is inert and the shared-hosting path is
 * byte-identical to before P6.
 */
class ScreenSaved implements ShouldBroadcast
{
    use Dispatchable;
    use InteractsWithSockets;
    use SerializesModels;

    /**
     * @param  array{id:int,name:string}  $by  the saving user (id + name)
     * @param  ?string  $originTab  the saving tab's `editorTabId` (request's
     *         X-Editor-Tab). The client suppresses an echo only when this
     *         matches ITS tab id, so a tab ignores its own save (even before
     *         its Reverb socket connects, when toOthers can't exclude it) while
     *         the SAME user's OTHER tabs still sync. A user-id or socket compare
     *         can't do both.
     */
    public function __construct(
        public int $projectId,
        public int $screenId,
        public ?string $updatedAt,
        public array $by,
        public ?string $originTab = null,
    ) {}

    /**
     * Only broadcast when the Reverb tier is active. Belt-and-suspenders with
     * the controller gate so the default lock transport never emits.
     */
    public function broadcastWhen(): bool
    {
        return Settings::get('collab.transport', 'lock') === 'reverb';
    }

    /**
     * Per-project presence channel. Authorization lives in routes/channels.php
     * (`project.{project}`) and mirrors the editor's access check.
     */
    public function broadcastOn(): PresenceChannel
    {
        return new PresenceChannel("project.{$this->projectId}");
    }

    public function broadcastAs(): string
    {
        return 'ScreenSaved';
    }

    /**
     * @return array{screen_id:int,updated_at:?string,by:array{id:int,name:string},origin_tab:?string}
     */
    public function broadcastWith(): array
    {
        return [
            'screen_id'  => $this->screenId,
            'updated_at' => $this->updatedAt,
            'by'         => $this->by,
            // isset() guards against jobs serialized before $originTab was added:
            // SerializesModels::__unserialize skips missing keys and PHP 8
            // leaves the typed property uninitialized (not null) in that case.
            'origin_tab' => isset($this->originTab) ? $this->originTab : null,
        ];
    }
}
