/**
 * Realtime collaboration transport seam (Team Workspaces P4 / Δ21 + P6 / Δ2).
 *
 * The editor talks to a `RealtimeAdapter` instead of a concrete transport so
 * Tier-1 (pessimistic project lock, polling) and Tier-2 (Reverb live concurrent
 * editing, SSH/VPS opt-in) coexist without forking call sites.
 *
 * - `LockAdapter` (Tier-1, ships in BOTH modes, the DEFAULT) — no persistent
 *   connection; the heartbeat/poll + take-over flow is owned by `useEditLock`.
 *   This is the shared-hosting-safe path and is selected whenever
 *   `collabTransport !== 'reverb'` OR Reverb can't connect.
 * - `ReverbAdapter` (Tier-2, OPT-IN) — opens an Echo presence-channel
 *   subscription and routes peer saves to `onRemoteSave` + presence diffs to
 *   `onPresence`. Selected ONLY when `collabTransport === 'reverb'` AND a
 *   Reverb client config is present AND Echo constructs. Any failure falls back
 *   to `LockAdapter` so the editor is never left unprotected (the documented
 *   Δ21 fallback) — honoring CLAUDE.md §Don'ts (Reverb is never the default).
 *
 * Echo / pusher-js are imported lazily inside `echo.ts` (no top-level side
 * effects), so the default lock path never pulls a websocket into the bundle's
 * runtime execution.
 */

import { createEcho, type EchoInstance, type ReverbClientConfig } from '@/echo';

export type CollabTransport = 'lock' | 'reverb';

/**
 * Stable, connection-independent id for THIS editor tab. Generated once per page
 * load (survives Inertia partial reloads; a full navigation mints a new one).
 * Sent on every save as `X-Editor-Tab` and echoed back in broadcasts as
 * `origin_tab`, so a tab reliably ignores its OWN save even BEFORE the Reverb
 * socket connects (when `X-Socket-ID` is still null and `toOthers()` can't
 * exclude it — the queued echo would otherwise arrive after connect and force a
 * spurious reload). A user-id compare can't do this (it would also mute the same
 * user's OTHER tabs); a socket compare can't (null pre-connect).
 */
export const editorTabId: string =
    globalThis.crypto?.randomUUID?.() ??
    `tab-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;

/** Peer save payload (matches ScreenSaved::broadcastWith). */
export type RemoteSavePayload = {
    screen_id: number;
    updated_at: string | null;
    by: { id: number; name: string };
    /** The saving tab's `editorTabId`. The editor suppresses an echo only when
     *  this matches OUR tab id — so the same user's OTHER tabs (different id)
     *  still sync, while our own save never reloads us. Null on legacy saves. */
    origin_tab: string | null;
};

/** Peer project-level edit payload (matches ProjectUpdated::broadcastWith). */
export type RemoteProjectUpdatePayload = {
    by: { id: number; name: string };
    /** See {@link RemoteSavePayload.origin_tab}. */
    origin_tab: string | null;
};

export type PresenceMember = { id: number; name: string };

/**
 * Live Reverb socket id of the active editor connection, or null on the lock
 * path / before the socket connects. The autosave fetch (persist.ts) sends it
 * as `X-Socket-ID` so the server's `broadcast()->toOthers()` excludes our own
 * socket — without it, raw `fetch` carries no socket id and Reverb echoes our
 * own ScreenSaved back to us (→ spurious reload + lost selection mid-edit).
 */
let activeEcho: EchoInstance | null = null;
export function reverbSocketId(): string | null {
    try {
        return activeEcho?.socketId() ?? null;
    } catch {
        return null;
    }
}

export type RealtimeConfig = {
    transport: CollabTransport;
    projectId: number;
    /** Reverb client config from the `collab.reverb` shared prop. Null on the
     *  default lock path (and when Reverb is misconfigured server-side). */
    reverb?: ReverbClientConfig | null;
    /** A peer saved a screen — the editor re-hydrates via its clean-refresh
     *  path. Only fired by the ReverbAdapter. */
    onRemoteSave?: (payload: RemoteSavePayload) => void;
    /** A peer made a project-level edit (rename / factors / locales) — the
     *  editor re-hydrates the `project` prop. Only fired by the ReverbAdapter.
     *  Carries `by` so the editor can ignore its OWN echoed-back broadcast
     *  (the autosave fetch can't send X-Socket-ID, so toOthers can't exclude us). */
    onRemoteProjectUpdate?: (payload: RemoteProjectUpdatePayload) => void;
    /** Presence roster changed (joined/left/snapshot). Tier-2 nicety. */
    onPresence?: (members: PresenceMember[]) => void;
};

export interface RealtimeAdapter {
    readonly transport: CollabTransport;
    /** Join the collaboration scope for a project. Tier-1 is a no-op (the
     *  lock heartbeat is owned by useEditLock); Tier-2 opens the websocket. */
    join(projectId: number): void;
    /** Tear down any transport-level resources. */
    dispose(): void;
}

/**
 * Tier-1 adapter — pessimistic project lock. No persistent connection; the
 * heartbeat/poll + take-over flow is driven by `useEditLock`. This adapter is
 * a placeholder seam so the editor doesn't branch on transport directly.
 */
export class LockAdapter implements RealtimeAdapter {
    readonly transport: CollabTransport = 'lock';

    join(_projectId: number): void {
        // No-op. useEditLock owns the heartbeat poll for Tier 1.
    }

    dispose(): void {
        // No-op.
    }
}

/**
 * Tier-2 adapter — Reverb live concurrent editing (OPT-IN, SSH/VPS only).
 * Subscribes to the per-project presence channel and routes peer `ScreenSaved`
 * broadcasts to `onRemoteSave` + presence membership to `onPresence`.
 *
 * Constructed only via the factory once Echo has built successfully — so the
 * editor never instantiates this on the default path. On any subscription
 * error it tears itself down (the factory has already returned, but the editor
 * keeps the lock heartbeat running underneath as the safety net).
 */
export class ReverbAdapter implements RealtimeAdapter {
    readonly transport: CollabTransport = 'reverb';

    private echo: EchoInstance;
    private channelName: string;
    private onRemoteSave?: (payload: RemoteSavePayload) => void;
    private onRemoteProjectUpdate?: (payload: RemoteProjectUpdatePayload) => void;
    private onPresence?: (members: PresenceMember[]) => void;
    private joined = false;
    // Live roster — `.here` seeds it, `.joining`/`.leaving` keep it current.
    // Each change fires `onPresence` with the FULL list so the UI can render
    // the current set without diffing.
    private roster: PresenceMember[] = [];

    constructor(echo: EchoInstance, config: RealtimeConfig) {
        this.echo = echo;
        // Publish this connection's socket for the autosave X-Socket-ID header.
        activeEcho = echo;
        // Echo's `.join()` prefixes `presence-` on the wire; the channel
        // authorized in routes/channels.php is `project.{project}`.
        this.channelName = `project.${config.projectId}`;
        this.onRemoteSave = config.onRemoteSave;
        this.onRemoteProjectUpdate = config.onRemoteProjectUpdate;
        this.onPresence = config.onPresence;
    }

    join(_projectId: number): void {
        if (this.joined) return;
        this.joined = true;
        try {
            const channel = this.echo.join(this.channelName);
            channel
                .here((members: PresenceMember[]) => {
                    this.roster = members ?? [];
                    this.onPresence?.(this.roster);
                })
                .joining((member: PresenceMember) => {
                    if (!this.roster.some((m) => m.id === member.id)) {
                        this.roster = [...this.roster, member];
                    }
                    this.onPresence?.(this.roster);
                })
                .leaving((member: PresenceMember) => {
                    this.roster = this.roster.filter((m) => m.id !== member.id);
                    this.onPresence?.(this.roster);
                })
                .listen('.ScreenSaved', (payload: RemoteSavePayload) =>
                    this.onRemoteSave?.(payload),
                )
                .listen('.ProjectUpdated', (payload: RemoteProjectUpdatePayload) =>
                    this.onRemoteProjectUpdate?.(payload),
                );
        } catch {
            // Subscription failed mid-flight — the lock heartbeat under the
            // editor is the safety net. Tear down so we don't leak a channel.
            this.dispose();
        }
    }

    dispose(): void {
        try {
            this.echo.leave(this.channelName);
        } catch {
            // Already gone — ignore.
        }
        if (activeEcho === this.echo) {
            activeEcho = null;
        }
        this.joined = false;
    }
}

/**
 * Factory. Returns a `ReverbAdapter` when `transport==='reverb'` AND a Reverb
 * client config is present AND Echo constructs; otherwise the `LockAdapter`
 * (the default + the documented fallback when Reverb is misconfigured or the
 * socket can't be built). The editor's lock heartbeat (`useEditLock`) keeps
 * running regardless, so a Reverb failure never leaves the editor unprotected.
 */
export function createRealtimeAdapter(config: RealtimeConfig): RealtimeAdapter {
    if (config.transport === 'reverb' && config.reverb) {
        try {
            const echo = createEcho(config.reverb);
            if (echo) {
                return new ReverbAdapter(echo, config);
            }
        } catch {
            // Fall through to the lock — never leave the editor unprotected.
        }
    }
    return new LockAdapter();
}
