import { useEvent } from 'expo'
import { useVideoPlayer, type VideoPlayer, type VideoSource } from 'expo-video'
import { useEffect, useRef } from 'react'
import { usePlaybackScope } from './PlaybackScope'

// Replaces three near-identical player wirings (feed cell, story segment,
// composer thumbnail) with one hook that registers itself with the
// nearest <PlaybackScope> and applies per-mode defaults.
//
// The three modes encode the *role* the player plays in the app, not the
// shape of its props — the hook owns the per-mode toggles internally so
// future contributors can't drift one site away from another:
//
//   feed     — chronological feed cell. Loops; muted unless this is the
//              audible holder; respects the parent scope's preload window
//              so off-screen players detach their native buffer.
//   story    — story-viewer segment. Plays unmuted, never loops, always
//              the audible holder while the modal is up.
//   composer — local muted preview thumbnail. Always plays, never claims
//              the arbiter, never participates in `currentVideoId`. Multi-
//              instance composers (rail of selected media) all loop in
//              parallel, exactly as today.
//
// `audible` toggles whether the hook claims the global audio slot when
// the parent scope's `currentVideoId` matches this hook's `videoId`. For
// `feed` and `story` it should be true (the user-facing default); for
// `composer` it is ignored — composer never claims.

export type PlayerMode = 'feed' | 'story' | 'composer'

export type UseScopedPlayerOptions = {
  // For feed cells the source can flip between concrete VideoSource and
  // null while off-screen (preload-window detachment). For story
  // segments the source flips between VideoSource and null when the user
  // advances to a photo node. For composer thumbs the URI is set on
  // first mount and stable thereafter.
  source: VideoSource | null
  // Stable id for the underlying *content*, not the React component
  // instance. For feed posts this is post.id; for story segments this
  // is the segment id; for composer thumbs the local URI works (no
  // arbiter participation, but still required so the scope can register
  // a unique entry).
  videoId: string
  mode: PlayerMode
  // Defaults: feed/story → true, composer → never claims regardless.
  audible?: boolean
}

export type UseScopedPlayerResult = {
  player: VideoPlayer
  // True when the underlying AVPlayer is rendering frames. Drives
  // poster/play-button overlays in feed cells.
  isPlaying: boolean
  // True when this hook currently holds its scope's `currentVideoId` AND
  // claims the global arbiter. Composer instances are never audible.
  isAudible: boolean
}

// Extracts a stable URI from a possibly-null VideoSource for change
// detection in `replaceAsync` effects. expo-video accepts either a bare
// string or `{ uri }` — we cover both shapes, returning null when the
// source itself is null or has no usable URI.
function sourceUri(source: VideoSource | null): string | null {
  if (!source) return null
  if (typeof source === 'string') return source
  return (source as { uri?: string }).uri ?? null
}

export function useScopedPlayer(opts: UseScopedPlayerOptions): UseScopedPlayerResult {
  const { source, videoId, mode } = opts
  // Composer never participates in audible coordination regardless of
  // what the caller passes — gate here so the rest of the hook can read
  // a single `wantsAudible` flag.
  const wantsAudible = mode === 'composer' ? false : (opts.audible ?? true)

  const scope = usePlaybackScope()
  const isCurrent = scope.currentVideoId === videoId
  // The preload window is feed-only — story (modal) and composer
  // (always-mounted thumbnails) play unconditionally. Skipping the
  // active check for those modes also avoids spurious tear-down when a
  // feed scope above the modal happens to mark a story id "inactive".
  const active = mode === 'feed' ? scope.isActive(videoId) : true
  // Audible only when this hook *would* be eligible AND its scope
  // currently nominates it. For feed/story the eligibility check is
  // wantsAudible + isCurrent; composer never qualifies.
  const isAudible = wantsAudible && isCurrent

  // Mode → setup callback. Runs only on first construction of the
  // native player (and again on hook-recreation, which expo-video does
  // not normally trigger). Per-render transitions live in the effects
  // below.
  const player = useVideoPlayer(active ? source : null, (p) => {
    switch (mode) {
      case 'feed':
        p.loop = true
        p.muted = !isAudible
        p.audioMixingMode = isAudible ? 'duckOthers' : 'mixWithOthers'
        p.staysActiveInBackground = isAudible
        p.preservesPitch = false
        if (active && isAudible) p.play()
        break
      case 'story':
        p.loop = false
        p.muted = !isAudible
        p.audioMixingMode = isAudible ? 'duckOthers' : 'mixWithOthers'
        p.staysActiveInBackground = isAudible
        p.preservesPitch = false
        // Story autoplay decision lives with the segment driver (the
        // [pid].tsx route flips paused/unpaused) — don't auto-play here.
        break
      case 'composer':
        p.loop = true
        p.muted = true
        p.audioMixingMode = 'mixWithOthers'
        p.staysActiveInBackground = false
        p.play()
        break
    }
  })

  // Register with the scope so blur-pause can find this player. Storing
  // `audible` on the entry lets future hardening (e.g. now-playing
  // metadata, dev-mode warnings about un-claimed audible players) read
  // it without re-walking the registry against `currentVideoId`.
  useEffect(() => {
    return scope.registerPlayer(videoId, { player, audible: wantsAudible })
  }, [scope, videoId, player, wantsAudible])

  // Recycle the native player when the underlying source URI changes
  // for an already-mounted instance. FlashList reuses the same React
  // tree across feed posts; the story viewer reuses one player across
  // segments. Without `replaceAsync` either flow allocates a fresh
  // AVPlayer per content swap.
  const lastUri = useRef<string | null>(null)
  useEffect(() => {
    if (!active) return
    const uri = sourceUri(source)
    if (!uri || uri === lastUri.current) return
    lastUri.current = uri
    if (source) player.replaceAsync(source).catch(() => {})
  }, [source, active, player])

  // Per-render transitions. `feed` is the only mode whose play/pause
  // and audible state varies during the player's lifetime — story
  // drives play/pause from the segment route (paused state), composer
  // is fire-and-forget.
  useEffect(() => {
    if (mode !== 'feed') return
    if (!active) {
      try {
        player.pause()
      } catch {}
      return
    }
    try {
      if (isAudible) player.play()
      else player.pause()
    } catch {}
  }, [mode, isAudible, active, player])

  // Audio-side transitions: mute, mixing mode, and the background-audio
  // capability flap together as one transaction so the known iOS quirk
  // (toggling one without the other) cannot occur. §5.4 / §12.6 of the
  // plan. Composer never participates — its player is always silent.
  useEffect(() => {
    if (mode === 'composer') return
    try {
      player.muted = !isAudible
      player.audioMixingMode = isAudible ? 'duckOthers' : 'mixWithOthers'
      player.staysActiveInBackground = isAudible
    } catch {}
  }, [mode, isAudible, player])

  const { isPlaying } = useEvent(player, 'playingChange', {
    isPlaying: player.playing,
    oldIsPlaying: undefined,
  })

  return { player, isPlaying, isAudible }
}
