/** Privacy-gated WebRTC transport for the shared office signaling contract. */
import {
  advanceScreenShareStreamCursor,
  isScreenShareGrantActive,
  parseScreenShareServerMessage,
} from "./signalingValidation.ts";
import {
  SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
  type ScreenShareAccessGrant,
  type ScreenShareBinding,
  type ScreenShareClientMessage,
  type ScreenShareCredential,
  type ScreenShareParticipant,
  type ScreenSharePeerMessage,
  type ScreenShareResumeGrant,
  type ScreenShareRole,
  type ScreenShareServerMessage,
  type ScreenShareSignalPayload,
  type ScreenShareStreamCursor,
} from "./signalingTypes.ts";
import type { MediaAuthorizationDecision } from "./types.ts";
import {
  fetchEphemeralIceConfiguration,
  type EphemeralIceConfiguration,
} from "./iceClient.ts";

export type RemoteMediaRole = ScreenShareRole;
export type RemoteMediaStatus = "idle" | "connecting" | "live" | "reconnecting" | "stopped" | "revoked" | "disposed";

export interface RemoteMediaConsent { authorized: boolean; optedIn: boolean }
export interface RemoteMediaState {
  role: RemoteMediaRole;
  status: RemoteMediaStatus;
  binding: ScreenShareBinding;
  sessionId: string | null;
  participantCount: number;
  reconnectAttempt: number;
  hasRemoteVideo: boolean;
}
export interface RemoteMediaEndpoints { join: string; signal: string; events: string; leave: string; ice: string }
export interface RemoteMediaTimers {
  setTimeout(callback: () => void, delayMs: number): ReturnType<typeof setTimeout>;
  clearTimeout(handle: ReturnType<typeof setTimeout>): void;
}
export interface RemoteOfficeMediaOptions {
  role: RemoteMediaRole;
  binding: ScreenShareBinding;
  /** Preferred: the app's authenticated fetch wrapper. Tokens remain in POST bodies. */
  authenticatedFetch?: typeof globalThis.fetch;
  fetch?: typeof globalThis.fetch;
  peerConnectionFactory?: (configuration?: RTCConfiguration) => RTCPeerConnection;
  /** Injectable for tests/deployments; called only with an active in-memory grant. */
  iceConfigurationProvider?(authorization: {
    binding: ScreenShareBinding;
    credential: ScreenShareCredential;
  }): Promise<EphemeralIceConfiguration>;
  endpoints?: Partial<RemoteMediaEndpoints>;
  timers?: RemoteMediaTimers;
  now?: () => number;
  reconnectBaseMs?: number;
  reconnectMaximumMs?: number;
  /** Bounded sender defaults for every presenter-to-viewer connection. */
  presenterEncodingPolicy?: Partial<ScreenShareEncodingPolicy>;
  onStateChange?(state: RemoteMediaState): void;
  onError?(error: Error): void;
}
export interface PresenterStart {
  consent: RemoteMediaConsent;
  /** Caller-owned. Exactly one live video track is published and never stopped here. */
  stream: MediaStream;
  video: HTMLVideoElement;
}
export type ViewerStart =
  | {
      /** A current `authorizeMediaSurface` decision with explicit viewer opt-in. */
      decision: MediaAuthorizationDecision;
      viewerOptIn?: never;
      video: HTMLVideoElement;
    }
  | {
      /**
       * Literal user action for signaling servers that perform authorization
       * during join and intentionally expose no source locator to the client.
       */
      decision?: never;
      viewerOptIn: true;
      video: HTMLVideoElement;
    };
export interface RemoteOfficeMedia {
  startPresenter(input: PresenterStart): Promise<void>;
  startViewer(input: ViewerStart): Promise<void>;
  state(): RemoteMediaState;
  stop(): Promise<void>;
  revoke(): Promise<void>;
  dispose(): Promise<void>;
}

export interface ScreenShareEncodingPolicy {
  maximumWidth: number;
  maximumHeight: number;
  maximumFrameRate: number;
  maximumBitrateBps: number;
  contentHint: "detail" | "text";
  degradationPreference: RTCDegradationPreference;
}

export interface ScreenShareEncodingPolicyResult {
  contentHintApplied: boolean;
  senderParametersApplied: boolean;
}

export const DEFAULT_SCREEN_SHARE_ENCODING_POLICY: Readonly<ScreenShareEncodingPolicy> =
  Object.freeze({
    maximumWidth: 1_280,
    maximumHeight: 720,
    maximumFrameRate: 15,
    maximumBitrateBps: 1_500_000,
    contentHint: "detail",
    degradationPreference: "maintain-resolution",
  });

interface PeerSlot {
  readonly id: string;
  readonly connection: RTCPeerConnection;
  readonly pendingIce: (RTCIceCandidateInit | null)[];
  senderPolicyReady: Promise<void>;
}
const ENDPOINTS: RemoteMediaEndpoints = {
  join: "/api/v1/media/join",
  signal: "/api/v1/media/signal",
  events: "/api/v1/media/events",
  leave: "/api/v1/media/leave",
  ice: "/api/v1/media/ice",
};
const TIMERS: RemoteMediaTimers = {
  setTimeout: (callback, delay) => globalThis.setTimeout(callback, delay),
  clearTimeout: (handle) => globalThis.clearTimeout(handle),
};

function copyBinding(value: ScreenShareBinding): ScreenShareBinding {
  for (const key of ["officeId", "levelId", "screenId"] as const) {
    if (typeof value[key] !== "string" || value[key].length < 1 || value[key].length > 128) {
      throw new TypeError(`remote media: invalid ${key}`);
    }
  }
  if (value.roomId !== null && (typeof value.roomId !== "string" || value.roomId.length < 1 || value.roomId.length > 128)) {
    throw new TypeError("remote media: invalid roomId");
  }
  return { ...value };
}
function sameBinding(a: ScreenShareBinding, b: ScreenShareBinding): boolean {
  return a.officeId === b.officeId && a.levelId === b.levelId && a.roomId === b.roomId && a.screenId === b.screenId;
}
function asError(reason: unknown, fallback: string): Error { return reason instanceof Error ? reason : new Error(fallback); }
function requestId(number: number): string { return `tera-media-${number}`; }
function sdp(value: RTCSessionDescriptionInit): ScreenShareSignalPayload {
  if ((value.type !== "offer" && value.type !== "answer") || typeof value.sdp !== "string") {
    throw new Error("remote media: local SDP is invalid");
  }
  return { kind: "sdp", descriptionType: value.type, sdp: value.sdp };
}
function remoteDescription(signal: Extract<ScreenShareSignalPayload, { kind: "sdp" }>): RTCSessionDescriptionInit {
  return { type: signal.descriptionType, sdp: signal.sdp };
}

function finitePositive(value: number | undefined, fallback: number, name: string): number {
  const resolved = value ?? fallback;
  if (!Number.isFinite(resolved) || resolved <= 0) {
    throw new RangeError(`remote media: ${name} must be finite and positive`);
  }
  return resolved;
}

function resolveEncodingPolicy(value: Partial<ScreenShareEncodingPolicy> | undefined): ScreenShareEncodingPolicy {
  const degradationPreference = value?.degradationPreference ??
    DEFAULT_SCREEN_SHARE_ENCODING_POLICY.degradationPreference;
  if (
    degradationPreference !== "balanced" &&
    degradationPreference !== "maintain-framerate" &&
    degradationPreference !== "maintain-resolution"
  ) throw new RangeError("remote media: invalid degradation preference");
  const contentHint = value?.contentHint ?? DEFAULT_SCREEN_SHARE_ENCODING_POLICY.contentHint;
  if (contentHint !== "detail" && contentHint !== "text") {
    throw new RangeError("remote media: invalid screen content hint");
  }
  return {
    maximumWidth: finitePositive(value?.maximumWidth, DEFAULT_SCREEN_SHARE_ENCODING_POLICY.maximumWidth, "maximumWidth"),
    maximumHeight: finitePositive(value?.maximumHeight, DEFAULT_SCREEN_SHARE_ENCODING_POLICY.maximumHeight, "maximumHeight"),
    maximumFrameRate: finitePositive(value?.maximumFrameRate, DEFAULT_SCREEN_SHARE_ENCODING_POLICY.maximumFrameRate, "maximumFrameRate"),
    maximumBitrateBps: finitePositive(value?.maximumBitrateBps, DEFAULT_SCREEN_SHARE_ENCODING_POLICY.maximumBitrateBps, "maximumBitrateBps"),
    contentHint,
    degradationPreference,
  };
}

/** Best-effort WebRTC policy: capability differences never abort signaling. */
export async function applyScreenShareEncodingPolicy(
  track: MediaStreamTrack,
  sender: RTCRtpSender,
  policyValue: Partial<ScreenShareEncodingPolicy> = {},
): Promise<ScreenShareEncodingPolicyResult> {
  const policy = resolveEncodingPolicy(policyValue);
  let contentHintApplied = false;
  try {
    track.contentHint = policy.contentHint;
    contentHintApplied = track.contentHint === policy.contentHint;
  } catch { /* Safari versions without writable contentHint still share safely. */ }

  try {
    if (typeof sender.getParameters !== "function" || typeof sender.setParameters !== "function") {
      return { contentHintApplied, senderParametersApplied: false };
    }
    const parameters = sender.getParameters();
    if (!Array.isArray(parameters.encodings) || parameters.encodings.length === 0) {
      return { contentHintApplied, senderParametersApplied: false };
    }
    let width = 0;
    let height = 0;
    try {
      const settings = track.getSettings?.();
      width = typeof settings?.width === "number" ? settings.width : 0;
      height = typeof settings?.height === "number" ? settings.height : 0;
    } catch { /* Some synthetic/browser tracks do not expose settings. */ }
    const scaleResolutionDownBy = Math.max(
      1,
      width > 0 ? width / policy.maximumWidth : 1,
      height > 0 ? height / policy.maximumHeight : 1,
    );
    for (const encoding of parameters.encodings) {
      encoding.maxBitrate = Math.floor(policy.maximumBitrateBps);
      encoding.maxFramerate = policy.maximumFrameRate;
      encoding.scaleResolutionDownBy = scaleResolutionDownBy;
    }
    // Older Safari builds omit this dictionary member; do not manufacture it
    // because their setParameters implementations may reject unknown fields.
    if ("degradationPreference" in parameters) {
      parameters.degradationPreference = policy.degradationPreference;
    }
    await sender.setParameters(parameters);
    return { contentHintApplied, senderParametersApplied: true };
  } catch {
    return { contentHintApplied, senderParametersApplied: false };
  }
}

/**
 * Capture-free and storage-free by construction. The adapter never invokes a
 * media acquisition API, calls `play`, records, uploads media, or exposes the
 * server grant in state/events. It owns peer connections and received tracks;
 * the presenter's input stream, video, and track remain caller-owned.
 */
export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): RemoteOfficeMedia {
  if (options.authenticatedFetch && options.fetch) throw new TypeError("remote media: provide one fetch adapter");
  const fetcher = options.authenticatedFetch ?? options.fetch ?? globalThis.fetch;
  if (typeof fetcher !== "function") throw new TypeError("remote media: fetch is required");
  const makePeer = options.peerConnectionFactory ?? ((configuration) => new RTCPeerConnection(configuration));
  const binding = copyBinding(options.binding);
  const endpoints = { ...ENDPOINTS, ...options.endpoints };
  const timers = options.timers ?? TIMERS;
  const now = options.now ?? Date.now;
  const reconnectBase = options.reconnectBaseMs ?? 500;
  const reconnectMaximum = options.reconnectMaximumMs ?? 15_000;
  const presenterEncodingPolicy = resolveEncodingPolicy(options.presenterEncodingPolicy);
  const provideIce = options.iceConfigurationProvider ?? ((authorization) =>
    fetchEphemeralIceConfiguration({
      authenticatedFetch: fetcher,
      endpoint: endpoints.ice,
      now,
      binding: authorization.binding,
      credential: authorization.credential,
    }));
  if (!(reconnectBase > 0) || !Number.isFinite(reconnectBase) || reconnectMaximum < reconnectBase) {
    throw new RangeError("remote media: invalid reconnect bounds");
  }

  let status: RemoteMediaStatus = "idle";
  let grant: ScreenShareAccessGrant | null = null;
  let clientSequence = 0;
  let counter = 0;
  let cursor: ScreenShareStreamCursor | null = null;
  let participants: readonly ScreenShareParticipant[] = [];
  let presenter: PresenterStart | null = null;
  let presenterOriginalContentHint: string | null = null;
  let viewer: ViewerStart | null = null;
  let receiverStream: MediaStream | null = null;
  let iceConfiguration: EphemeralIceConfiguration | null = null;
  let iceAuthorization: ScreenShareCredential | null = null;
  let eventAbort: AbortController | null = null;
  let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  let renewalTimer: ReturnType<typeof setTimeout> | null = null;
  let reconnectAttempt = 0;
  let generation = 0;
  const peers = new Map<string, PeerSlot>();

  function state(): RemoteMediaState {
    return {
      role: options.role,
      status,
      binding: { ...binding },
      sessionId: grant?.credential.sessionId ?? null,
      participantCount: peers.size,
      reconnectAttempt,
      hasRemoteVideo: receiverStream !== null,
    };
  }
  function changed(): void { options.onStateChange?.(state()); }
  function setStatus(next: RemoteMediaStatus): void { status = next; changed(); }
  function envelope(): { protocolVersion: 1; sequence: number; timestampMs: number } {
    const result = { protocolVersion: SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION, sequence: clientSequence, timestampMs: now() };
    clientSequence += 1;
    return result;
  }

  async function post(endpoint: string, message: ScreenShareClientMessage, signal?: AbortSignal): Promise<Response> {
    return fetcher(endpoint, {
      method: "POST",
      credentials: "same-origin",
      cache: "no-store",
      redirect: "error",
      headers: { Accept: "application/json", "Content-Type": "application/json" },
      body: JSON.stringify(message),
      signal,
    });
  }
  async function parsedResponse(response: Response): Promise<ScreenShareServerMessage> {
    if (!response.ok) throw new Error(`remote media request failed (${response.status})`);
    const parsed = parseScreenShareServerMessage(await response.json() as unknown);
    if (!parsed.ok) throw new Error(parsed.error);
    return parsed.value;
  }

  function releaseReceiver(): void {
    if (receiverStream) for (const track of receiverStream.getTracks()) track.stop();
    if (viewer?.video.srcObject === receiverStream) viewer.video.srcObject = null;
    receiverStream = null;
  }
  function closePeer(id: string): void {
    const slot = peers.get(id);
    if (!slot) return;
    slot.connection.onicecandidate = null;
    slot.connection.ontrack = null;
    slot.connection.onconnectionstatechange = null;
    slot.connection.close();
    peers.delete(id);
  }
  function closeTransport(): void {
    generation += 1;
    eventAbort?.abort();
    eventAbort = null;
    if (reconnectTimer !== null) timers.clearTimeout(reconnectTimer);
    reconnectTimer = null;
    if (renewalTimer !== null) timers.clearTimeout(renewalTimer);
    renewalTimer = null;
    for (const id of [...peers.keys()]) closePeer(id);
    releaseReceiver();
    iceConfiguration = null;
    iceAuthorization = null;
    if (presenterOriginalContentHint !== null) {
      const track = presenter?.stream.getVideoTracks()[0];
      try { if (track) track.contentHint = presenterOriginalContentHint; } catch { /* best effort */ }
      presenterOriginalContentHint = null;
    }
  }

  function resetPeerMesh(): void {
    for (const id of [...peers.keys()]) closePeer(id);
    releaseReceiver();
  }

  async function refreshIceConfiguration(expectedGrant: ScreenShareAccessGrant): Promise<void> {
    const activeGeneration = generation;
    const next = await provideIce({ binding: { ...binding }, credential: { ...expectedGrant.credential } });
    if (generation !== activeGeneration || grant !== expectedGrant || !isScreenShareGrantActive(expectedGrant, now())) {
      throw new Error("remote media: ICE configuration was superseded");
    }
    if (!Number.isSafeInteger(next.expiresAtMs) || next.expiresAtMs <= now() || !next.configuration.iceServers?.length) {
      throw new Error("remote media: ICE configuration is not active");
    }
    iceConfiguration = {
      configuration: { ...next.configuration, iceServers: next.configuration.iceServers.map((server) => ({ ...server })) },
      expiresAtMs: next.expiresAtMs,
    };
    iceAuthorization = { ...expectedGrant.credential };
  }

  async function ensureIceConfiguration(): Promise<void> {
    if (!grant) throw new Error("remote media: signaling grant is missing");
    const credential = grant.credential;
    if (iceConfiguration && iceConfiguration.expiresAtMs > now() && iceAuthorization &&
        iceAuthorization.sessionId === credential.sessionId &&
        iceAuthorization.participantId === credential.participantId &&
        iceAuthorization.role === credential.role &&
        iceAuthorization.grantToken === credential.grantToken) return;
    await refreshIceConfiguration(grant);
  }

  async function sendSignal(targetParticipantId: string, signal: ScreenShareSignalPayload): Promise<void> {
    if (!grant) throw new Error("remote media: signaling grant is missing");
    const request: ScreenShareClientMessage = {
      type: "screen-share-signal-request",
      ...envelope(),
      binding,
      credential: grant.credential,
      targetParticipantId,
      signal,
    };
    const response = await post(endpoints.signal, request);
    if (!response.ok) throw new Error(`remote media signal failed (${response.status})`);
  }

  function configurePeer(participant: ScreenShareParticipant): PeerSlot {
    const prior = peers.get(participant.participantId);
    if (prior) return prior;
    const activeGeneration = generation;
    if (!iceConfiguration || iceConfiguration.expiresAtMs <= now()) {
      throw new Error("remote media: active ICE configuration is required before creating a peer");
    }
    const connection = makePeer(iceConfiguration.configuration);
    let senderPolicyReady = Promise.resolve();
    const slot: PeerSlot = {
      id: participant.participantId,
      connection,
      pendingIce: [],
      senderPolicyReady,
    };
    peers.set(slot.id, slot);
    connection.onicecandidate = (event) => {
      if (generation !== activeGeneration) return;
      const signal: ScreenShareSignalPayload = event.candidate
        ? {
            kind: "ice-candidate",
            candidate: event.candidate.candidate,
            sdpMid: event.candidate.sdpMid,
            sdpMLineIndex: event.candidate.sdpMLineIndex,
            usernameFragment: event.candidate.usernameFragment,
          }
        : { kind: "ice-complete" };
      void sendSignal(slot.id, signal).catch((reason) => options.onError?.(asError(reason, "remote media ICE failed")));
    };
    connection.onconnectionstatechange = () => {
      if (connection.connectionState === "connected") { reconnectAttempt = 0; setStatus("live"); }
      else if (connection.connectionState === "failed" || connection.connectionState === "disconnected") scheduleReconnect();
    };
    if (options.role === "presenter") {
      const track = presenter?.stream.getVideoTracks()[0];
      if (!track || track.readyState !== "live") throw new Error("remote media: presenter track ended");
      if (presenterOriginalContentHint === null) presenterOriginalContentHint = track.contentHint;
      const sender = connection.addTrack(track, presenter!.stream);
      senderPolicyReady = applyScreenShareEncodingPolicy(track, sender, presenterEncodingPolicy).then(() => undefined);
      slot.senderPolicyReady = senderPolicyReady;
    } else {
      connection.addTransceiver("video", { direction: "recvonly" });
      connection.ontrack = (event) => {
        if (generation !== activeGeneration || status === "revoked" || status === "disposed") {
          event.track.stop();
          return;
        }
        releaseReceiver();
        receiverStream = event.streams[0] ?? new MediaStream([event.track]);
        viewer!.video.srcObject = receiverStream;
        changed();
      };
    }
    changed();
    return slot;
  }

  async function offer(participant: ScreenShareParticipant): Promise<void> {
    const slot = configurePeer(participant);
    await slot.senderPolicyReady;
    const connection = slot.connection;
    const description = await connection.createOffer();
    await connection.setLocalDescription(description);
    await sendSignal(participant.participantId, sdp(connection.localDescription ?? description));
  }

  async function reconcile(next: readonly ScreenShareParticipant[]): Promise<void> {
    if (!grant) return;
    const own = grant.credential.participantId;
    const allowed = next.filter((item) => item.participantId !== own && item.role !== options.role);
    const ids = new Set(allowed.map((item) => item.participantId));
    for (const id of [...peers.keys()]) if (!ids.has(id)) closePeer(id);
    participants = allowed;
    if (allowed.some((participant) => !peers.has(participant.participantId))) await ensureIceConfiguration();
    if (options.role === "presenter") {
      for (const participant of allowed) if (!peers.has(participant.participantId)) await offer(participant);
    } else {
      const presenterPeer = allowed.find((item) => item.role === "presenter");
      if (presenterPeer) configurePeer(presenterPeer);
    }
    changed();
  }

  async function applyPeerMessage(message: ScreenSharePeerMessage): Promise<void> {
    if (!grant || message.sessionId !== grant.credential.sessionId || !sameBinding(message.binding, binding)) return;
    const advanced = advanceScreenShareStreamCursor(cursor, message);
    if (!advanced.ok) return;
    cursor = advanced.value;
    if (message.type === "screen-share-stopped") {
      closeTransport();
      // The server has already destroyed the signaling session. Forget the
      // capability now so caller cleanup cannot issue a redundant leave that
      // is guaranteed to fail authorization.
      grant = null;
      setStatus("stopped");
      return;
    }
    if (message.type === "screen-share-revoked") {
      if (message.scope === "session" || message.targetParticipantId === grant.credential.participantId) {
        closeTransport();
        grant = null;
        setStatus("revoked");
      } else if (message.targetParticipantId) closePeer(message.targetParticipantId);
      return;
    }
    if (message.type === "screen-share-participants") {
      await reconcile(message.participants);
      return;
    }
    if (message.targetParticipantId !== grant.credential.participantId) return;
    const from = participants.find((item) => item.participantId === message.fromParticipantId);
    if (!from) return;
    if (!peers.has(from.participantId)) await ensureIceConfiguration();
    const connection = configurePeer(from).connection;
    if (message.signal.kind === "ice-complete") {
      if (connection.remoteDescription) await connection.addIceCandidate(null);
      else peers.get(from.participantId)?.pendingIce.push(null);
      return;
    }
    if (message.signal.kind === "ice-candidate") {
      const incoming: RTCIceCandidateInit = {
        candidate: message.signal.candidate,
        sdpMid: message.signal.sdpMid,
        sdpMLineIndex: message.signal.sdpMLineIndex,
        usernameFragment: message.signal.usernameFragment ?? undefined,
      };
      if (connection.remoteDescription) await connection.addIceCandidate(incoming);
      else peers.get(from.participantId)?.pendingIce.push(incoming);
      return;
    }
    await connection.setRemoteDescription(remoteDescription(message.signal));
    const slot = peers.get(from.participantId);
    if (slot) {
      for (const incoming of slot.pendingIce.splice(0)) await connection.addIceCandidate(incoming);
    }
    if (message.signal.descriptionType === "offer") {
      const answer = await connection.createAnswer();
      await connection.setLocalDescription(answer);
      await sendSignal(from.participantId, sdp(connection.localDescription ?? answer));
    }
  }

  async function readEvents(response: Response, activeGeneration: number, expectedRequestId: string): Promise<void> {
    if (!response.ok || !response.body) throw new Error(`remote media events failed (${response.status})`);
    const reader = response.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";
    let first = true;
    try {
      while (generation === activeGeneration) {
        const chunk = await reader.read();
        if (generation !== activeGeneration || chunk.done) break;
        buffer += decoder.decode(chunk.value, { stream: true }).replace(/\r/g, "");
        let boundary = buffer.indexOf("\n\n");
        while (boundary >= 0) {
          const block = buffer.slice(0, boundary);
          buffer = buffer.slice(boundary + 2);
          boundary = buffer.indexOf("\n\n");
          const data = block.split("\n").filter((line) => line.startsWith("data:"))
            .map((line) => line.slice(5).replace(/^ /, "")).join("\n");
          if (!data) continue;
          let raw: unknown;
          try { raw = JSON.parse(data); } catch { continue; }
          const parsed = parseScreenShareServerMessage(raw);
          if (!parsed.ok) continue;
          if (first) {
            first = false;
            if (parsed.value.type !== "screen-share-resume-grant" ||
                parsed.value.requestId !== expectedRequestId || !(await acceptGrant(parsed.value))) {
              throw new Error("remote media: event stream did not start with a valid resume grant");
            }
          } else if (parsed.value.type === "screen-share-signal-relay" ||
                     parsed.value.type === "screen-share-participants" ||
                     parsed.value.type === "screen-share-stopped" ||
                     parsed.value.type === "screen-share-revoked") {
            await applyPeerMessage(parsed.value);
          }
        }
      }
      if (generation === activeGeneration && first) throw new Error("remote media: event stream ended before resume grant");
    } finally { reader.releaseLock(); }
  }

  async function acceptGrant(message: ScreenShareResumeGrant): Promise<boolean> {
    const current = grant?.credential;
    if (!current || message.grant.credential.sessionId !== current.sessionId ||
        message.grant.credential.participantId !== current.participantId ||
        !sameBinding(message.binding, binding) || message.grant.credential.role !== options.role ||
        !isScreenShareGrantActive(message.grant, now())) return false;
    if (grant && (message.grant.credential.sessionId !== grant.credential.sessionId ||
        message.grant.credential.participantId !== grant.credential.participantId)) return false;
    grant = message.grant;
    clientSequence = message.nextClientSequence;
    cursor = { sessionId: message.grant.credential.sessionId, sequence: message.sequence, timestampMs: message.timestampMs };
    if (!message.continuous) resetPeerMesh();
    await reconcile(message.participants);
    scheduleRenewal();
    // A presenter with no viewers is still live: the hosted signaling session
    // is accepting authorized viewers. A viewer remains connecting until its
    // receive-only peer actually reaches the connected state.
    setStatus(options.role === "presenter" && peers.size === 0 ? "live" : "connecting");
    return true;
  }

  function scheduleRenewal(): void {
    if (!grant || status === "revoked" || status === "stopped" || status === "disposed") return;
    if (renewalTimer !== null) timers.clearTimeout(renewalTimer);
    const delay = Math.max(1_000, grant.expiresAtMs - now() - 15_000);
    renewalTimer = timers.setTimeout(() => {
      renewalTimer = null;
      void startEvents().catch((reason) => {
        options.onError?.(asError(reason, "remote media lease renewal failed"));
        scheduleReconnect();
      });
    }, delay);
  }

  function scheduleReconnect(): void {
    if (!grant || status === "revoked" || status === "stopped" || status === "disposed" || reconnectTimer !== null) return;
    // A failed RTCPeerConnection is not recoverable by renewing the signaling
    // lease alone. Tear down the peer mesh and let the resume snapshot rebuild
    // it; presenter input tracks remain untouched and reusable.
    generation += 1;
    eventAbort?.abort();
    eventAbort = null;
    if (renewalTimer !== null) timers.clearTimeout(renewalTimer);
    renewalTimer = null;
    resetPeerMesh();
    setStatus("reconnecting");
    const delay = Math.min(reconnectMaximum, reconnectBase * 2 ** reconnectAttempt);
    reconnectAttempt += 1;
    reconnectTimer = timers.setTimeout(() => {
      reconnectTimer = null;
      void startEvents().catch((reason) => { options.onError?.(asError(reason, "remote media reconnect failed")); scheduleReconnect(); });
    }, delay);
  }

  async function startEvents(): Promise<void> {
    if (!grant) throw new Error("remote media: grant is missing");
    const activeGeneration = generation;
    eventAbort?.abort();
    const abort = new AbortController();
    eventAbort = abort;
    const request: ScreenShareClientMessage = {
      type: "screen-share-resume-request",
      ...envelope(),
      requestId: requestId(++counter),
      binding,
      credential: grant.credential,
      lastReceivedSequence: cursor?.sequence ?? 0,
    };
    try {
      const response = await post(endpoints.events, request, abort.signal);
      await readEvents(response, activeGeneration, request.requestId);
      if (generation === activeGeneration) scheduleReconnect();
    } catch (reason) {
      if (generation !== activeGeneration || abort.signal.aborted) return;
      throw reason;
    }
  }

  async function join(role: RemoteMediaRole, activeGeneration: number): Promise<void> {
    const id = requestId(++counter);
    const message: ScreenShareClientMessage = role === "presenter"
      ? { type: "screen-share-create-request", ...envelope(), requestId: id, binding, role }
      : { type: "screen-share-join-request", ...envelope(), requestId: id, binding, role, viewerOptIn: true };
    const response = await parsedResponse(await post(endpoints.join, message));
    const expected = role === "presenter" ? "screen-share-create-grant" : "screen-share-join-grant";
    if (response.type !== expected || response.requestId !== id || !sameBinding(response.binding, binding) ||
        response.grant.credential.role !== role || !isScreenShareGrantActive(response.grant, now())) {
      throw new Error("remote media: join grant is invalid");
    }
    if (generation !== activeGeneration || status === "disposed" || status === "revoked" || status === "stopped") {
      throw new Error("remote media: join was superseded");
    }
    grant = response.grant;
    clientSequence = response.nextClientSequence;
    cursor = { sessionId: response.grant.credential.sessionId, sequence: response.sequence, timestampMs: response.timestampMs };
    // Signaling authorization always precedes TURN. This is the first point at
    // which an ICE request can carry a server-issued credential and binding.
    await refreshIceConfiguration(response.grant);
    await reconcile(response.participants);
    scheduleRenewal();
    if (role === "presenter" && peers.size === 0) setStatus("live");
    void startEvents().catch((reason) => { options.onError?.(asError(reason, "remote media events failed")); scheduleReconnect(); });
  }

  async function startPresenter(input: PresenterStart): Promise<void> {
    if (options.role !== "presenter") throw new Error("remote media: adapter is not a presenter");
    if (status === "disposed" || status === "revoked") throw new Error("remote media: session is terminal");
    if (!input.consent.authorized || !input.consent.optedIn) throw new Error("remote media: authorization and explicit opt-in are required");
    const tracks = input.stream.getVideoTracks().filter((track) => track.readyState === "live");
    if (tracks.length !== 1) throw new Error("remote media: presenter requires exactly one live screen track");
    input.video.autoplay = false;
    input.video.muted = true;
    closeTransport();
    grant = null;
    participants = [];
    presenter = input;
    viewer = null;
    setStatus("connecting");
    try {
      await join("presenter", generation);
    } catch (reason) {
      if (status === "connecting") {
        // A join can succeed while the grant-bound TURN exchange fails. End
        // that just-created hosted session with its in-memory capability
        // instead of abandoning it until lease expiry. Caller capture remains
        // untouched and can continue as a local preview.
        try { await leave(); } catch { closeTransport(); grant = null; participants = []; }
        presenter = null;
        setStatus("idle");
      }
      throw reason;
    }
  }

  async function startViewer(input: ViewerStart): Promise<void> {
    if (options.role !== "viewer") throw new Error("remote media: adapter is not a viewer");
    if (status === "disposed" || status === "revoked") throw new Error("remote media: session is terminal");
    const ready = input.decision === undefined
      ? input.viewerOptIn === true
      : input.decision.authorized && input.decision.optedIn && input.decision.canView &&
        input.decision.surface.screenId === binding.screenId && input.decision.surface.officeId === binding.officeId &&
        input.decision.surface.source?.kind === "live-stream";
    if (!ready) {
      throw new Error("remote media: a ready live-stream authorization decision is required");
    }
    input.video.autoplay = false;
    input.video.muted = true;
    closeTransport();
    grant = null;
    participants = [];
    viewer = input;
    presenter = null;
    setStatus("connecting");
    try {
      await join("viewer", generation);
    } catch (reason) {
      if (status === "connecting") {
        try { await leave(); } catch { closeTransport(); grant = null; participants = []; }
        viewer = null;
        setStatus("idle");
      }
      throw reason;
    }
  }

  async function leave(): Promise<void> {
    const credential = grant?.credential;
    closeTransport();
    participants = [];
    grant = null;
    if (!credential) return;
    // A presenter ending the whole share is always a stop. `revoke` is a
    // policy word locally, not permission to claim a moderator action on wire.
    const message: ScreenShareClientMessage = options.role === "presenter"
      ? {
          type: "screen-share-stop-request", ...envelope(), requestId: requestId(++counter), binding,
          credential: credential as ScreenShareCredential<"presenter">, reason: "presenter-stopped",
        }
      : {
          type: "screen-share-revoke-request", ...envelope(), requestId: requestId(++counter), binding,
          credential, scope: "participant",
          targetParticipantId: credential.participantId,
          reason: "viewer-left",
        };
    const response = await post(endpoints.leave, message);
    if (!response.ok) throw new Error(`remote media leave failed (${response.status})`);
  }

  async function stop(): Promise<void> {
    if (status === "disposed" || status === "stopped") return;
    try { await leave(); } finally { presenter = null; viewer = null; setStatus("stopped"); }
  }
  async function revoke(): Promise<void> {
    if (status === "disposed" || status === "revoked") return;
    try { await leave(); } finally { presenter = null; viewer = null; setStatus("revoked"); }
  }
  async function dispose(): Promise<void> {
    if (status === "disposed") return;
    try { await leave(); } finally { presenter = null; viewer = null; status = "disposed"; changed(); }
  }

  return { startPresenter, startViewer, state, stop, revoke, dispose };
}
