// Single global "audio slot" — at most one PlaybackScope can hold it at a
// time.
//
// The contract:
//   - `claim(scopeId, release)` makes the caller the new holder. If a
//     different scope previously held the slot, its `release` callback
//     fires synchronously (no microtask gap) before the new claim wins.
//     The caller's own callback is invoked only when somebody *else*
//     subsequently claims the slot or this scope explicitly yields.
//   - `yield(scopeId)` clears the slot iff the caller is the current
//     holder; calling `yield` from a non-holder is a no-op (avoids races
//     where two scopes each think they were the most recent holder).
//   - Release callbacks must be idempotent and side-effect-only — the
//     arbiter does not invoke a scope's release callback when that same
//     scope re-claims the slot, even with a different release function.
//     A scope yielding to itself ("I no longer want the audio slot")
//     should call `yield`, not re-`claim`.
//
// Lives outside React on purpose: scopes mount/unmount independently, and
// a stack-push -> blur -> claim handover happens in the same frame; a
// React-only coordinator is one render too late.

type Holder = { scopeId: string; release: () => void }

class PlaybackArbiter {
  private holder: Holder | null = null

  claim(scopeId: string, release: () => void): void {
    const prev = this.holder
    this.holder = { scopeId, release }
    if (prev && prev.scopeId !== scopeId) {
      try {
        prev.release()
      } catch {
        // Release callbacks should not throw; swallow so a buggy scope
        // can't wedge the arbiter for everyone else.
      }
    }
  }

  yield(scopeId: string): void {
    if (this.holder?.scopeId === scopeId) this.holder = null
  }

  has(scopeId: string): boolean {
    return this.holder?.scopeId === scopeId
  }
}

export const playbackArbiter = new PlaybackArbiter()

// Exposed only for unit tests so each test can start from a known state.
export function __resetPlaybackArbiterForTests() {
  ;(playbackArbiter as unknown as { holder: Holder | null }).holder = null
}
