#![allow(clippy::missing_errors_doc, clippy::must_use_candidate)]

use core::ffi::{c_char, c_void};
use core::ptr;
use std::ffi::{CStr, CString};
use std::path::Path;

use serde::de::DeserializeOwned;
use serde::Deserialize;

use crate::asset::{Asset, Size};
use crate::error::{from_swift, AVPlayerError};
use crate::ffi;
use crate::metadata::MetadataItem;
use crate::time::Time;

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PlayerInfoPayload {
    status: i32,
    error_message: Option<String>,
    rate: f32,
    current_time: Time,
    duration: Time,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PlayerItemInfoPayload {
    status: i32,
    error_message: Option<String>,
    duration: Time,
    presentation_size: Size,
    metadata: Vec<MetadataItem>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PlayerItemEventPayload {
    event: String,
    status: Option<i32>,
    error_message: Option<String>,
    presentation_size: Option<Size>,
    has_originating_participant: Option<bool>,
    recommended_time_offset_from_live: Option<Time>,
}

/// `AVPlayerStatus`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlayerStatus {
    /// Mirrors the `AVPlayer` framework case `Unknown`.
    Unknown,
    /// Mirrors the `AVPlayer` framework case `ReadyToPlay`.
    ReadyToPlay,
    /// Mirrors the `AVPlayer` framework case `Failed`.
    Failed,
}

impl PlayerStatus {
    /// Mirrors the `AVPlayer` framework constant `fn`.
    #[must_use]
    pub const fn from_raw(raw: i32) -> Self {
        match raw {
            1 => Self::ReadyToPlay,
            2 => Self::Failed,
            _ => Self::Unknown,
        }
    }
}

/// `AVPlayerItemStatus`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlayerItemStatus {
    /// Mirrors the `AVPlayer` framework case `Unknown`.
    Unknown,
    /// Mirrors the `AVPlayer` framework case `ReadyToPlay`.
    ReadyToPlay,
    /// Mirrors the `AVPlayer` framework case `Failed`.
    Failed,
}

impl PlayerItemStatus {
    /// Mirrors the `AVPlayer` framework constant `fn`.
    #[must_use]
    pub const fn from_raw(raw: i32) -> Self {
        match raw {
            1 => Self::ReadyToPlay,
            2 => Self::Failed,
            _ => Self::Unknown,
        }
    }
}

/// Events emitted by `PlayerItemObserver`.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum PlayerItemEvent {
    /// Mirrors the `AVPlayer` framework case `StatusChanged`.
    StatusChanged {
        status: PlayerItemStatus,
        error_message: Option<String>,
    },
    /// Mirrors the `AVPlayer` framework case `PresentationSizeChanged`.
    PresentationSizeChanged(Size),
    /// Mirrors the `AVPlayer` framework case `TimeJumped`.
    TimeJumped { has_originating_participant: bool },
    /// Mirrors the `AVPlayer` framework case `DidPlayToEnd`.
    DidPlayToEnd,
    /// Mirrors the `AVPlayer` framework case `FailedToPlayToEnd`.
    FailedToPlayToEnd { error_message: Option<String> },
    /// Mirrors the `AVPlayer` framework case `PlaybackStalled`.
    PlaybackStalled,
    /// Mirrors the `AVPlayer` framework case `NewAccessLogEntry`.
    NewAccessLogEntry,
    /// Mirrors the `AVPlayer` framework case `NewErrorLogEntry`.
    NewErrorLogEntry,
    /// Mirrors the `AVPlayer` framework case `RecommendedTimeOffsetFromLiveDidChange`.
    RecommendedTimeOffsetFromLiveDidChange(Time),
    /// Mirrors the `AVPlayer` framework case `MediaSelectionChanged`.
    MediaSelectionChanged,
}

struct PlayerItemObserverState {
    callback: Box<dyn Fn(PlayerItemEvent) + Send + 'static>,
}

struct PeriodicTimeObserverState {
    callback: Box<dyn FnMut(Time) + Send + 'static>,
}

struct BoundaryTimeObserverState {
    callback: Box<dyn FnMut() + Send + 'static>,
}

/// Safe wrapper around `AVPlayerItem`.
#[derive(Debug)]
pub struct PlayerItem {
    pub(crate) ptr: *mut c_void,
}

impl Drop for PlayerItem {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            // SAFETY: `self.ptr` is a valid, non-null handle returned by the corresponding
            // ffi create function and has not been released.
            unsafe { ffi::av_player_item_release(self.ptr) };
            self.ptr = ptr::null_mut();
        }
    }
}

impl PlayerItem {
    /// Create a player item from a file path.
    pub fn from_file_path(path: impl AsRef<Path>) -> Result<Self, AVPlayerError> {
        let path = path
            .as_ref()
            .to_str()
            .ok_or_else(|| AVPlayerError::InvalidArgument("path is not valid UTF-8".into()))?;
        Self::from_url_internal(path, true)
    }

    /// Create a player item from a remote URL.
    pub fn from_remote_url(url: impl AsRef<str>) -> Result<Self, AVPlayerError> {
        Self::from_url_internal(url.as_ref(), false)
    }

    /// Create a player item from an existing asset.
    pub fn from_asset(asset: &Asset) -> Result<Self, AVPlayerError> {
        let keys_json =
            CString::new("[\"duration\",\"tracks\",\"metadata\"]").map_err(|error| {
                AVPlayerError::InvalidArgument(format!("asset-key JSON contains NUL byte: {error}"))
            })?;
        let mut err: *mut c_char = ptr::null_mut();
        // SAFETY: `asset.ptr` is a valid borrowed AVAsset handle, `keys_json` is a
        // NUL-terminated string, and `err` points to writable storage for the bridge.
        let ptr = unsafe {
            ffi::av_player_item_create_with_asset(asset.ptr, keys_json.as_ptr(), &mut err)
        };
        if ptr.is_null() {
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
        }
        Ok(Self { ptr })
    }

    fn from_url_internal(url: &str, is_file_url: bool) -> Result<Self, AVPlayerError> {
        let url = CString::new(url).map_err(|error| {
            AVPlayerError::InvalidArgument(format!("URL contains NUL byte: {error}"))
        })?;
        let keys_json =
            CString::new("[\"duration\",\"tracks\",\"metadata\"]").map_err(|error| {
                AVPlayerError::InvalidArgument(format!("asset-key JSON contains NUL byte: {error}"))
            })?;
        let mut err: *mut c_char = ptr::null_mut();
        // SAFETY: `url` and `keys_json` are NUL-terminated strings owned by this
        // frame, and `err` points to writable storage for the bridge.
        let ptr = unsafe {
            ffi::av_player_item_create_with_url(
                url.as_ptr(),
                is_file_url,
                keys_json.as_ptr(),
                &mut err,
            )
        };
        if ptr.is_null() {
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
        }
        Ok(Self { ptr })
    }

    fn info(&self) -> Result<PlayerItemInfoPayload, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        // SAFETY: `self.ptr` is a valid AVPlayerItem handle and `err` points to
        // writable storage for the bridge.
        let json_ptr = unsafe { ffi::av_player_item_info_json(self.ptr, &mut err) };
        if json_ptr.is_null() {
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
        }
        parse_json_and_free(json_ptr)
    }

    /// Calls the `AVPlayer` framework counterpart for `status`.
    pub fn status(&self) -> Result<PlayerItemStatus, AVPlayerError> {
        Ok(PlayerItemStatus::from_raw(self.info()?.status))
    }

    /// Calls the `AVPlayer` framework counterpart for `error`.
    pub fn error(&self) -> Result<Option<String>, AVPlayerError> {
        Ok(self.info()?.error_message)
    }

    /// Calls the `AVPlayer` framework counterpart for `duration`.
    pub fn duration(&self) -> Result<Time, AVPlayerError> {
        Ok(self.info()?.duration)
    }

    /// Calls the `AVPlayer` framework counterpart for `presentation_size`.
    pub fn presentation_size(&self) -> Result<Size, AVPlayerError> {
        Ok(self.info()?.presentation_size)
    }

    /// The current macOS SDK does not expose `externalMetadata`; this returns
    /// the underlying asset metadata instead.
    pub fn metadata(&self) -> Result<Vec<MetadataItem>, AVPlayerError> {
        Ok(self.info()?.metadata)
    }

    /// Calls the `AVPlayer` framework counterpart for `observe`.
    pub fn observe<F>(&self, callback: F) -> Result<PlayerItemObserver, AVPlayerError>
    where
        F: Fn(PlayerItemEvent) + Send + 'static,
    {
        let state = Box::new(PlayerItemObserverState {
            callback: Box::new(callback),
        });
        let userdata = Box::into_raw(state).cast::<c_void>();
        let mut err: *mut c_char = ptr::null_mut();
        // SAFETY: `self.ptr` is a valid AVPlayerItem handle, `userdata` is a Box
        // allocation that remains alive until the drop callback runs, and `err`
        // points to writable storage for the bridge.
        let token = unsafe {
            ffi::av_player_item_add_observer(
                self.ptr,
                Some(player_item_event_trampoline),
                userdata,
                Some(player_item_observer_drop),
                &mut err,
            )
        };
        if token.is_null() {
            // SAFETY: Observer creation failed before ownership of `userdata` was
            // transferred to the bridge, so we must reclaim it locally.
            unsafe { player_item_observer_drop(userdata) };
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
        }
        Ok(PlayerItemObserver { token })
    }
}

/// KVO + notification observer for `AVPlayerItem`.
#[derive(Debug)]
pub struct PlayerItemObserver {
    token: *mut c_void,
}

impl Drop for PlayerItemObserver {
    fn drop(&mut self) {
        if !self.token.is_null() {
            // SAFETY: `self.token` is a valid observer token returned by the bridge
            // and has not been released yet.
            unsafe { ffi::av_player_item_observer_release(self.token) };
            self.token = ptr::null_mut();
        }
    }
}

/// Safe wrapper around `AVPlayer`.
#[derive(Debug)]
pub struct Player {
    pub(crate) ptr: *mut c_void,
}

impl Drop for Player {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            // SAFETY: `self.ptr` is a valid, non-null handle returned by the corresponding
            // ffi create function and has not been released.
            unsafe { ffi::av_player_release(self.ptr) };
            self.ptr = ptr::null_mut();
        }
    }
}

impl Player {
    /// Create a player from a file path.
    pub fn from_file_path(path: impl AsRef<Path>) -> Result<Self, AVPlayerError> {
        let path = path
            .as_ref()
            .to_str()
            .ok_or_else(|| AVPlayerError::InvalidArgument("path is not valid UTF-8".into()))?;
        Self::from_url_internal(path, true)
    }

    /// Create a player from a remote URL.
    pub fn from_remote_url(url: impl AsRef<str>) -> Result<Self, AVPlayerError> {
        Self::from_url_internal(url.as_ref(), false)
    }

    /// Create a player that plays the supplied asset.
    pub fn from_asset(asset: &Asset) -> Result<Self, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        // SAFETY: `asset.ptr` is a valid borrowed AVAsset handle and `err` points
        // to writable storage for the bridge.
        let ptr = unsafe { ffi::av_player_create_with_asset(asset.ptr, &mut err) };
        if ptr.is_null() {
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
        }
        Ok(Self { ptr })
    }

    /// Create a player from an already-configured item.
    pub fn from_item(item: &PlayerItem) -> Result<Self, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        // SAFETY: `item.ptr` is a valid borrowed AVPlayerItem handle and `err`
        // points to writable storage for the bridge.
        let ptr = unsafe { ffi::av_player_create_with_item(item.ptr, &mut err) };
        if ptr.is_null() {
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
        }
        Ok(Self { ptr })
    }

    fn from_url_internal(url: &str, is_file_url: bool) -> Result<Self, AVPlayerError> {
        let url = CString::new(url).map_err(|error| {
            AVPlayerError::InvalidArgument(format!("URL contains NUL byte: {error}"))
        })?;
        let mut err: *mut c_char = ptr::null_mut();
        // SAFETY: `url` is a NUL-terminated string owned by this frame and `err`
        // points to writable storage for the bridge.
        let ptr = unsafe { ffi::av_player_create_with_url(url.as_ptr(), is_file_url, &mut err) };
        if ptr.is_null() {
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(ffi::status::PLAYER_CREATE_FAILED, err) });
        }
        Ok(Self { ptr })
    }

    fn info(&self) -> Result<PlayerInfoPayload, AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        // SAFETY: `self.ptr` is a valid AVPlayer handle and `err` points to
        // writable storage for the bridge.
        let json_ptr = unsafe { ffi::av_player_info_json(self.ptr, &mut err) };
        if json_ptr.is_null() {
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(ffi::status::OPERATION_FAILED, err) });
        }
        parse_json_and_free(json_ptr)
    }

    /// Calls the `AVPlayer` framework counterpart for `status`.
    pub fn status(&self) -> Result<PlayerStatus, AVPlayerError> {
        Ok(PlayerStatus::from_raw(self.info()?.status))
    }

    /// Calls the `AVPlayer` framework counterpart for `error`.
    pub fn error(&self) -> Result<Option<String>, AVPlayerError> {
        Ok(self.info()?.error_message)
    }

    /// Calls the `AVPlayer` framework counterpart for `rate`.
    pub fn rate(&self) -> Result<f32, AVPlayerError> {
        Ok(self.info()?.rate)
    }

    /// Calls the `AVPlayer` framework counterpart for `current_time`.
    pub fn current_time(&self) -> Result<Time, AVPlayerError> {
        Ok(self.info()?.current_time)
    }

    /// Calls the `AVPlayer` framework counterpart for `duration`.
    pub fn duration(&self) -> Result<Time, AVPlayerError> {
        Ok(self.info()?.duration)
    }

    /// Calls the `AVPlayer` framework counterpart for `current_item`.
    pub fn current_item(&self) -> Option<PlayerItem> {
        // SAFETY: `self.ptr` is a valid AVPlayer handle; the bridge returns either
        // a retained current item or null when no item is set.
        let ptr = unsafe { ffi::av_player_copy_current_item(self.ptr) };
        if ptr.is_null() {
            None
        } else {
            Some(PlayerItem { ptr })
        }
    }

    /// Calls the `AVPlayer` framework counterpart for `play`.
    pub fn play(&self) {
        // SAFETY: `self.ptr` is a valid, non-null handle returned by the corresponding
        // ffi create function and has not been released.
        unsafe { ffi::av_player_play(self.ptr) };
    }

    /// Calls the `AVPlayer` framework counterpart for `pause`.
    pub fn pause(&self) {
        // SAFETY: `self.ptr` is a valid, non-null handle returned by the corresponding
        // ffi create function and has not been released.
        unsafe { ffi::av_player_pause(self.ptr) };
    }

    /// Calls the `AVPlayer` framework counterpart for `set_rate`.
    pub fn set_rate(&self, rate: f32) {
        // SAFETY: `self.ptr` is a valid, non-null handle returned by the corresponding
        // ffi create function and has not been released.
        unsafe { ffi::av_player_set_rate(self.ptr, rate) };
    }

    /// Calls the `AVPlayer` framework counterpart for `seek_to`.
    pub fn seek_to(&self, time: Time) -> Result<(), AVPlayerError> {
        let mut err: *mut c_char = ptr::null_mut();
        let (value, timescale, kind) = time.to_raw();
        // SAFETY: `self.ptr` is a valid AVPlayer handle and `err` points to
        // writable storage for the bridge.
        let status = unsafe { ffi::av_player_seek(self.ptr, value, timescale, kind, &mut err) };
        if status != ffi::status::OK {
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(status, err) });
        }
        Ok(())
    }

    /// Calls the `AVPlayer` framework counterpart for `add_periodic_time_observer`.
    pub fn add_periodic_time_observer<F>(
        &self,
        interval: Time,
        queue_label: Option<&str>,
        callback: F,
    ) -> Result<PeriodicTimeObserver, AVPlayerError>
    where
        F: FnMut(Time) + Send + 'static,
    {
        let queue_label = queue_label_cstring(queue_label)?;
        let state = Box::new(PeriodicTimeObserverState {
            callback: Box::new(callback),
        });
        let userdata = Box::into_raw(state).cast::<c_void>();
        let (value, timescale, kind) = interval.to_raw();
        let mut err: *mut c_char = ptr::null_mut();
        // SAFETY: `self.ptr` is a valid AVPlayer handle, `userdata` is a Box
        // allocation that remains alive until the drop callback runs, and `err`
        // points to writable storage for the bridge.
        let token = unsafe {
            ffi::av_player_add_periodic_time_observer(
                self.ptr,
                value,
                timescale,
                kind,
                queue_label
                    .as_ref()
                    .map_or(ptr::null(), |label| label.as_ptr()),
                Some(periodic_time_observer_trampoline),
                userdata,
                Some(periodic_time_observer_drop),
                &mut err,
            )
        };
        if token.is_null() {
            // SAFETY: Observer creation failed before ownership of `userdata` was
            // transferred to the bridge, so we must reclaim it locally.
            unsafe { periodic_time_observer_drop(userdata) };
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
        }
        Ok(PeriodicTimeObserver { token })
    }

    /// Calls the `AVPlayer` framework counterpart for `add_boundary_time_observer`.
    pub fn add_boundary_time_observer<F>(
        &self,
        times: &[Time],
        queue_label: Option<&str>,
        callback: F,
    ) -> Result<BoundaryTimeObserver, AVPlayerError>
    where
        F: FnMut() + Send + 'static,
    {
        let queue_label = queue_label_cstring(queue_label)?;
        let times_json = serde_json::to_string(times).map_err(|error| {
            AVPlayerError::InvalidArgument(format!("failed to encode boundary times: {error}"))
        })?;
        let times_json = CString::new(times_json).map_err(|error| {
            AVPlayerError::InvalidArgument(format!(
                "boundary times JSON contains NUL byte: {error}"
            ))
        })?;
        let state = Box::new(BoundaryTimeObserverState {
            callback: Box::new(callback),
        });
        let userdata = Box::into_raw(state).cast::<c_void>();
        let mut err: *mut c_char = ptr::null_mut();
        // SAFETY: `self.ptr` is a valid AVPlayer handle, `times_json` is a
        // NUL-terminated string owned by this frame, `userdata` is kept alive
        // until the drop callback runs, and `err` points to writable storage.
        let token = unsafe {
            ffi::av_player_add_boundary_time_observer(
                self.ptr,
                times_json.as_ptr(),
                queue_label
                    .as_ref()
                    .map_or(ptr::null(), |label| label.as_ptr()),
                Some(boundary_time_observer_trampoline),
                userdata,
                Some(boundary_time_observer_drop),
                &mut err,
            )
        };
        if token.is_null() {
            // SAFETY: Observer creation failed before ownership of `userdata` was
            // transferred to the bridge, so we must reclaim it locally.
            unsafe { boundary_time_observer_drop(userdata) };
            // SAFETY: On failure the bridge initializes `err` to an owned Swift error
            // payload that `from_swift` consumes.
            return Err(unsafe { from_swift(ffi::status::OBSERVER_FAILED, err) });
        }
        Ok(BoundaryTimeObserver { token })
    }
}

/// RAII token for `addPeriodicTimeObserver`.
#[derive(Debug)]
pub struct PeriodicTimeObserver {
    token: *mut c_void,
}

impl Drop for PeriodicTimeObserver {
    fn drop(&mut self) {
        if !self.token.is_null() {
            // SAFETY: `self.token` is a valid observer token returned by the bridge
            // and has not been released yet.
            unsafe { ffi::av_player_time_observer_release(self.token) };
            self.token = ptr::null_mut();
        }
    }
}

/// RAII token for `addBoundaryTimeObserver`.
#[derive(Debug)]
pub struct BoundaryTimeObserver {
    token: *mut c_void,
}

impl Drop for BoundaryTimeObserver {
    fn drop(&mut self) {
        if !self.token.is_null() {
            // SAFETY: `self.token` is a valid observer token returned by the bridge
            // and has not been released yet.
            unsafe { ffi::av_player_time_observer_release(self.token) };
            self.token = ptr::null_mut();
        }
    }
}

// SAFETY: AVPlayer / AVPlayerItem ObjC handles and observer tokens are safe to
// transfer across thread boundaries; method calls are internally dispatched
// safely.
unsafe impl Send for PlayerItem {}
unsafe impl Send for PlayerItemObserver {}
unsafe impl Send for Player {}
unsafe impl Send for PeriodicTimeObserver {}
unsafe impl Send for BoundaryTimeObserver {}

unsafe extern "C" fn player_item_event_trampoline(
    userdata: *mut c_void,
    payload_json: *const c_char,
) {
    if userdata.is_null() || payload_json.is_null() {
        return;
    }

    // SAFETY: `userdata` is a valid `*mut PlayerItemObserverState` allocated in
    // `observe()` and kept alive for the lifetime of the token; null is checked above.
    let callback = unsafe { &*userdata.cast::<PlayerItemObserverState>() };
    // SAFETY: `payload_json` is a non-null, NUL-terminated string owned by the
    // bridge for the duration of this callback.
    let Ok(payload) = unsafe { CStr::from_ptr(payload_json) }.to_str() else {
        return;
    };
    let Ok(payload) = serde_json::from_str::<PlayerItemEventPayload>(payload) else {
        return;
    };

    let event = match payload.event.as_str() {
        "status_changed" => PlayerItemEvent::StatusChanged {
            status: PlayerItemStatus::from_raw(payload.status.unwrap_or_default()),
            error_message: payload.error_message,
        },
        "presentation_size_changed" => match payload.presentation_size {
            Some(size) => PlayerItemEvent::PresentationSizeChanged(size),
            None => return,
        },
        "time_jumped" => PlayerItemEvent::TimeJumped {
            has_originating_participant: payload.has_originating_participant.unwrap_or(false),
        },
        "did_play_to_end" => PlayerItemEvent::DidPlayToEnd,
        "failed_to_play_to_end" => PlayerItemEvent::FailedToPlayToEnd {
            error_message: payload.error_message,
        },
        "playback_stalled" => PlayerItemEvent::PlaybackStalled,
        "new_access_log_entry" => PlayerItemEvent::NewAccessLogEntry,
        "new_error_log_entry" => PlayerItemEvent::NewErrorLogEntry,
        "recommended_time_offset_from_live_did_change" => {
            match payload.recommended_time_offset_from_live {
                Some(time) => PlayerItemEvent::RecommendedTimeOffsetFromLiveDidChange(time),
                None => return,
            }
        }
        "media_selection_changed" => PlayerItemEvent::MediaSelectionChanged,
        _ => return,
    };

    crate::util::catch_cb_panic("player_item_event_trampoline", || {
        (callback.callback)(event);
    });
}

unsafe extern "C" fn player_item_observer_drop(userdata: *mut c_void) {
    if !userdata.is_null() {
        // SAFETY: `userdata` is the unique Box pointer created in `observe()`; the
        // Swift bridge calls this drop callback exactly once.
        drop(unsafe { Box::from_raw(userdata.cast::<PlayerItemObserverState>()) });
    }
}

unsafe extern "C" fn periodic_time_observer_trampoline(
    userdata: *mut c_void,
    value: i64,
    timescale: i32,
    kind: i32,
) {
    if userdata.is_null() {
        return;
    }
    // SAFETY: `userdata` is a valid `*mut PeriodicTimeObserverState` allocated in
    // `add_periodic_time_observer()` and kept alive for the lifetime of the token;
    // null is checked above.
    let state = unsafe { &mut *userdata.cast::<PeriodicTimeObserverState>() };
    crate::util::catch_cb_panic("periodic_time_observer_trampoline", || {
        (state.callback)(Time::from_raw(value, timescale, kind));
    });
}

unsafe extern "C" fn periodic_time_observer_drop(userdata: *mut c_void) {
    if !userdata.is_null() {
        // SAFETY: `userdata` is the unique Box pointer created in
        // `add_periodic_time_observer()`; the Swift bridge calls this drop callback
        // exactly once.
        drop(unsafe { Box::from_raw(userdata.cast::<PeriodicTimeObserverState>()) });
    }
}

unsafe extern "C" fn boundary_time_observer_trampoline(userdata: *mut c_void) {
    if userdata.is_null() {
        return;
    }
    // SAFETY: `userdata` is a valid `*mut BoundaryTimeObserverState` allocated in
    // `add_boundary_time_observer()` and kept alive for the lifetime of the token;
    // null is checked above.
    let state = unsafe { &mut *userdata.cast::<BoundaryTimeObserverState>() };
    crate::util::catch_cb_panic("boundary_time_observer_trampoline", || {
        (state.callback)();
    });
}

unsafe extern "C" fn boundary_time_observer_drop(userdata: *mut c_void) {
    if !userdata.is_null() {
        // SAFETY: `userdata` is the unique Box pointer created in
        // `add_boundary_time_observer()`; the Swift bridge calls this drop callback
        // exactly once.
        drop(unsafe { Box::from_raw(userdata.cast::<BoundaryTimeObserverState>()) });
    }
}

fn queue_label_cstring(queue_label: Option<&str>) -> Result<Option<CString>, AVPlayerError> {
    queue_label
        .map(|label| {
            CString::new(label).map_err(|error| {
                AVPlayerError::InvalidArgument(format!("queue label contains NUL byte: {error}"))
            })
        })
        .transpose()
}

fn parse_json_and_free<T: DeserializeOwned>(json_ptr: *mut c_char) -> Result<T, AVPlayerError> {
    // SAFETY: `json_ptr` is a non-null, NUL-terminated C string returned by the
    // bridge; it remains valid until freed with `avp_string_free`.
    let json = unsafe { CStr::from_ptr(json_ptr) }
        .to_string_lossy()
        .into_owned();
    // SAFETY: `json_ptr` was returned by the FFI and has not been freed yet.
    unsafe { ffi::avp_string_free(json_ptr) };
    serde_json::from_str::<T>(&json).map_err(|error| {
        AVPlayerError::OperationFailed(format!("failed to decode bridge JSON: {error}"))
    })
}
