<?php
/**
 * Frontend Hooks, Shortcode, Block and REST.
 *
 * @package The Studio Player
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Register the Player-Builder custom post type.
 *
 * @return void
 */
function thespl_register_playlist_cpt() {
	$playlist_manage_cap = function_exists( 'thespl_get_min_capability' ) ? thespl_get_min_capability() : 'edit_others_posts';
	if ( ! is_string( $playlist_manage_cap ) || '' === $playlist_manage_cap ) {
		$playlist_manage_cap = 'edit_others_posts';
	}

	$labels = array(
		'name'               => __( 'Player-Builder', 'the-studio-player' ),
		'singular_name'      => __( 'Player-Builder', 'the-studio-player' ),
		'add_new'            => __( 'Build New Player', 'the-studio-player' ),
		'add_new_item'       => __( 'Build New Player', 'the-studio-player' ),
		'edit_item'          => __( 'Player-Builder', 'the-studio-player' ),
		'new_item'           => __( 'New Player-Builder', 'the-studio-player' ),
		'view_item'          => __( 'View Player-Builder', 'the-studio-player' ),
		'search_items'       => __( 'Search Player-Builder', 'the-studio-player' ),
		'not_found'          => __( 'No Player found - build a new one', 'the-studio-player' ),
		'not_found_in_trash' => __( 'No Player-Builder found in trash', 'the-studio-player' ),
		'menu_name'          => __( 'Player-Builder', 'the-studio-player' ),
	);

	$args = array(
		'labels'              => $labels,
		'public'              => false,
		'publicly_queryable'  => false,
		'show_ui'             => true,
		'show_in_menu'        => 'the-studio-player',
		'show_in_rest'        => true,
		'exclude_from_search' => true,
		'show_in_nav_menus'   => false,
		'supports'            => array( 'title' ),
		'map_meta_cap'        => true,
		'capabilities'        => array(
			'edit_post'          => 'edit_post',
			'read_post'          => 'read_post',
			'delete_post'        => 'delete_post',
			'edit_posts'         => $playlist_manage_cap,
			'edit_others_posts'  => $playlist_manage_cap,
			'publish_posts'      => $playlist_manage_cap,
			'read_private_posts' => $playlist_manage_cap,
			'create_posts'       => $playlist_manage_cap,
		),
		'has_archive'         => false,
		'rewrite'             => false,
		'menu_icon'           => 'dashicons-playlist-audio',
	);

	register_post_type( 'thespl_playlist', $args );
}
add_action( 'init', 'thespl_register_playlist_cpt' );

/**
 * Build a shortcode string for a given playlist and template.
 *
 * @param int    $playlist_id Playlist post ID.
 * @param string $template    Template slug.
 * @return string
 */
function thespl_get_player_shortcode_for_playlist( $playlist_id, $template = '' ) {
	$playlist_id = (int) $playlist_id;
	if ( $playlist_id <= 0 ) {
		return '';
	}

	$template = is_string( $template ) ? sanitize_key( $template ) : '';

	$atts = ' playlist_id="' . $playlist_id . '"';
	if ( '' !== $template ) {
		$atts .= ' template="' . $template . '"';
	}

	return '[thespl_player' . $atts . ']';
}

/**
 * Register REST API routes for The Studio Player.
 *
 * @return void
 */
function thespl_register_rest_routes() {
	register_rest_route(
		'thespl/v1',
		'/playlist/(?P<id>\d+)',
		array(
			'methods'             => 'GET',
			'callback'            => 'thespl_rest_get_playlist',
			'permission_callback' => 'thespl_rest_can_view_playlist',
			'args'                => array(
				'id' => array(
					'validate_callback' => static function ( $param ) {
						return is_numeric( $param ) && (int) $param > 0;
					},
				),
			),
		)
	);
	register_rest_route(
		'thespl/v1',
		'/magic-palette',
		array(
			'methods'             => 'POST',
			'callback'            => 'thespl_rest_magic_palette',
			'permission_callback' => 'thespl_rest_can_magic_palette',
		)
	);
}
add_action( 'rest_api_init', 'thespl_register_rest_routes' );

/**
 * Permission callback: Determine whether the current user may view a playlist via REST.
 *
 * @param WP_REST_Request $request Request instance.
 * @return bool True if access is allowed, false otherwise.
 */
function thespl_rest_can_view_playlist( WP_REST_Request $request ) {
	$id = (int) $request->get_param( 'id' );
	if ( $id <= 0 ) {
		return false;
	}

	$post = get_post( $id );
	if ( ! $post || 'thespl_playlist' !== $post->post_type ) {
		return false;
	}

	// Published playlists are public.
	if ( 'publish' === $post->post_status ) {
		return true;
	}

	// Non-public playlists only for editors of that post.
	return current_user_can( 'edit_post', $id );
}

/**
 * Permission callback: Determine whether the current user may generate a magic palette.
 *
 * @param WP_REST_Request $request Request instance.
 * @return bool True if access is allowed, false otherwise.
 */
function thespl_rest_can_magic_palette( WP_REST_Request $request ) {
	$playlist_id = (int) $request->get_param( 'playlist_id' );
	if ( $playlist_id <= 0 ) {
		return false;
	}

	return thespl_current_user_can_policy( 'playlist_edit_own', array( 'object_id' => $playlist_id ) );
}

/**
 * REST: Fetch playlist payload for frontend.
 *
 * @param WP_REST_Request $request Request object.
 * @return WP_Error|WP_REST_Response
 */
function thespl_rest_get_playlist( WP_REST_Request $request ) {
	$id   = (int) $request->get_param( 'id' );
	$post = get_post( $id );
	if ( ! $post || 'thespl_playlist' !== $post->post_type ) {
		return new WP_Error( 'thespl_not_found', __( 'Playlist not found', 'the-studio-player' ), array( 'status' => 404 ) );
	}
	if ( 'publish' !== $post->post_status && ! current_user_can( 'edit_post', $id ) ) {
		return new WP_Error( 'thespl_not_found', __( 'Playlist not found', 'the-studio-player' ), array( 'status' => 404 ) );
	}

	$raw_json = get_post_meta( $id, '_thespl_playlist_json', true );
	$raw_json = is_string( $raw_json ) ? $raw_json : '';
	$decoded  = array();
	if ( '' !== $raw_json ) {
		$decoded = json_decode( $raw_json, true );
		if ( ! is_array( $decoded ) ) {
			$decoded = array();
		}
	}

	$variant_engine = thespl_get_variant_engine();

	$fallback_variants = $variant_engine->get_display_fallback_variants( $decoded );

	$tracks = array();
	if ( isset( $decoded['tracks'] ) && is_array( $decoded['tracks'] ) ) {
		foreach ( $decoded['tracks'] as $t ) {
			if ( ! is_array( $t ) ) {
				continue;
			}

			$audio_id = isset( $t['audio_attachment_id'] ) ? (int) $t['audio_attachment_id'] : 0;
			if ( $audio_id <= 0 ) {
				continue;
			}
			$audio_file_path = get_attached_file( $audio_id );
			$audio_ext       = is_string( $audio_file_path ) ? strtolower( pathinfo( $audio_file_path, PATHINFO_EXTENSION ) ) : '';
			$audio_mime      = get_post_mime_type( $audio_id );
			$audio_mime      = is_string( $audio_mime ) ? strtolower( $audio_mime ) : '';
			$allowed_ext     = array( 'flac', 'wav' );
			$allowed_mime    = array( 'audio/flac', 'audio/x-flac', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/vnd.wave' );
			if ( ! in_array( $audio_ext, $allowed_ext, true ) && ! in_array( $audio_mime, $allowed_mime, true ) ) {
				continue;
			}

			$track_variants = $variant_engine->get_display_track_variants( $t, $fallback_variants );

			$waveform_id = 0;
			$cover_id    = isset( $t['cover_attachment_id'] ) ? (int) $t['cover_attachment_id'] : 0;

			$audio_specs = thespl_get_audio_tech_meta( $audio_id );

			$track_variant_info = $variant_engine->get_display_track_variant_info( $t, count( $track_variants ) );

			$guessed = thespl_guess_related_media_for_audio( $audio_id );
			if ( $cover_id <= 0 && isset( $guessed['cover'] ) && is_array( $guessed['cover'] ) ) {
				$cover_id = isset( $guessed['cover']['attachment_id'] ) ? (int) $guessed['cover']['attachment_id'] : 0;
			}
			if ( $waveform_id <= 0 && isset( $guessed['waveform'] ) && is_array( $guessed['waveform'] ) ) {
				$waveform_id = isset( $guessed['waveform']['attachment_id'] ) ? (int) $guessed['waveform']['attachment_id'] : 0;
			}

			$tracks[] = array(
				'title'        => isset( $t['title'] ) && is_string( $t['title'] ) ? $t['title'] : '',
				'artist'       => isset( $t['artist'] ) && is_string( $t['artist'] ) ? $t['artist'] : '',
				'credits'      => isset( $t['credits'] ) && is_string( $t['credits'] ) ? $t['credits'] : '',
				'variants'     => $track_variants,
				'variant_info' => $track_variant_info,
				'social'       => isset( $t['social'] ) && is_string( $t['social'] ) ? $t['social'] : '',
				'audio'        => array(
					'attachment_id'   => $audio_id,
					'stream_url'      => thespl_get_stream_url_for_attachment( $audio_id ),
					'channels'        => isset( $audio_specs['channels'] ) ? (int) $audio_specs['channels'] : 0,
					'sample_rate'     => isset( $audio_specs['sample_rate'] ) ? (int) $audio_specs['sample_rate'] : 0,
					'bits_per_sample' => isset( $audio_specs['bits_per_sample'] ) ? (int) $audio_specs['bits_per_sample'] : 0,
					'bitrate'         => isset( $audio_specs['bitrate'] ) ? (int) $audio_specs['bitrate'] : 0,
				),
				'waveform'     => array(
					'attachment_id' => $waveform_id,
					'url'           => $waveform_id > 0 ? wp_get_attachment_url( $waveform_id ) : ( isset( $guessed['waveform']['url'] ) ? (string) $guessed['waveform']['url'] : '' ),
				),
				'cover'        => array(
					'attachment_id' => $cover_id,
					'url'           => $cover_id > 0 ? wp_get_attachment_image_url( $cover_id, 'medium' ) : ( isset( $guessed['cover']['url'] ) ? (string) $guessed['cover']['url'] : '' ),
				),
			);
		}
	}

	$variants = $fallback_variants;
	if ( empty( $variants ) && ! empty( $tracks ) && isset( $tracks[0]['variants'] ) && is_array( $tracks[0]['variants'] ) ) {
		$variants = $tracks[0]['variants'];
	}

	$response = array(
		'id'           => $id,
		'title'        => get_the_title( $id ),
		'variants'     => $variants,
		'max_variants' => $variant_engine->get_editor_variant_count(),
		'tracks'       => $tracks,
	);

	return rest_ensure_response( $response );
}

/**
 * Convert a hex color to RGB components.
 *
 * @param string $hex Hex color string.
 * @return int[] Array with [r, g, b].
 */
function thespl_hex_to_rgb( $hex ) {
	$hex = is_string( $hex ) ? trim( $hex ) : '';
	$hex = ltrim( $hex, '#' );
	if ( 3 === strlen( $hex ) ) {
		$r = hexdec( str_repeat( $hex[0], 2 ) );
		$g = hexdec( str_repeat( $hex[1], 2 ) );
		$b = hexdec( str_repeat( $hex[2], 2 ) );
		return array( $r, $g, $b );
	}
	if ( 6 === strlen( $hex ) ) {
		$r = hexdec( substr( $hex, 0, 2 ) );
		$g = hexdec( substr( $hex, 2, 2 ) );
		$b = hexdec( substr( $hex, 4, 2 ) );
		return array( $r, $g, $b );
	}
	return array( 0, 0, 0 );
}

/**
 * Convert RGB components to a hex color.
 *
 * @param int $r Red.
 * @param int $g Green.
 * @param int $b Blue.
 * @return string Hex color.
 */
function thespl_rgb_to_hex( $r, $g, $b ) {
	$r = max( 0, min( 255, (int) $r ) );
	$g = max( 0, min( 255, (int) $g ) );
	$b = max( 0, min( 255, (int) $b ) );
	return '#' . str_pad( dechex( $r ), 2, '0', STR_PAD_LEFT ) . str_pad( dechex( $g ), 2, '0', STR_PAD_LEFT ) . str_pad( dechex( $b ), 2, '0', STR_PAD_LEFT );
}

/**
 * Mix two hex colors.
 *
 * @param string $hex1  First color.
 * @param string $hex2  Second color.
 * @param float  $ratio Mix ratio (0..1).
 * @return string Mixed hex color.
 */
function thespl_mix_hex( $hex1, $hex2, $ratio ) {
	$ratio = max( 0.0, min( 1.0, (float) $ratio ) );
	$a     = thespl_hex_to_rgb( $hex1 );
	$b     = thespl_hex_to_rgb( $hex2 );
	$r     = ( $a[0] * ( 1 - $ratio ) ) + ( $b[0] * $ratio );
	$g     = ( $a[1] * ( 1 - $ratio ) ) + ( $b[1] * $ratio );
	$bl    = ( $a[2] * ( 1 - $ratio ) ) + ( $b[2] * $ratio );
	return thespl_rgb_to_hex( $r, $g, $bl );
}

/**
 * Calculate relative brightness (simple luma) of a hex color.
 *
 * @param string $hex Hex color.
 * @return float Luma in range 0..1.
 */
function thespl_luma_hex( $hex ) {
	$rgb = thespl_hex_to_rgb( $hex );
	$r   = $rgb[0] / 255;
	$g   = $rgb[1] / 255;
	$b   = $rgb[2] / 255;
	return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
}

/**
 * Calculate WCAG relative luminance of a hex color.
 *
 * @param string $hex Hex color.
 * @return float Relative luminance.
 */
function thespl_relative_luminance_hex( $hex ) {
	$rgb       = thespl_hex_to_rgb( $hex );
	$to_linear = function ( $c ) {
		$c = $c / 255;
		return ( $c <= 0.03928 ) ? ( $c / 12.92 ) : pow( ( $c + 0.055 ) / 1.055, 2.4 );
	};
	$r         = $to_linear( $rgb[0] );
	$g         = $to_linear( $rgb[1] );
	$b         = $to_linear( $rgb[2] );
	return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
}

/**
 * Calculate WCAG contrast ratio between two hex colors.
 *
 * @param string $hex1 First color.
 * @param string $hex2 Second color.
 * @return float Contrast ratio.
 */
function thespl_contrast_ratio_hex( $hex1, $hex2 ) {
	$l1      = thespl_relative_luminance_hex( $hex1 );
	$l2      = thespl_relative_luminance_hex( $hex2 );
	$lighter = max( $l1, $l2 );
	$darker  = min( $l1, $l2 );
	return ( $lighter + 0.05 ) / ( $darker + 0.05 );
}

/**
 * Choose a readable text color (black/white) for a given background.
 *
 * @param string $bg_hex Background color.
 * @return string Text hex color.
 */
function thespl_best_text_on_bg( $bg_hex ) {
	$bg    = is_string( $bg_hex ) ? sanitize_hex_color( $bg_hex ) : '#111111';
	$black = '#111111';
	$white = '#ffffff';
	$rb    = thespl_contrast_ratio_hex( $black, $bg );
	$rw    = thespl_contrast_ratio_hex( $white, $bg );
	if ( $rb >= 4.5 && $rb >= $rw ) {
		return $black;
	}
	if ( $rw >= 4.5 ) {
		return $white;
	}
	return ( $rb >= $rw ) ? $black : $white;
}

/**
 * Convert RGB components to HSL.
 *
 * @param int $r Red.
 * @param int $g Green.
 * @param int $b Blue.
 * @return float[] Array with [h, s, l] in 0..1.
 */
function thespl_rgb_to_hsl_components( $r, $g, $b ) {
	$r  /= 255;
	$g  /= 255;
	$b  /= 255;
	$max = max( $r, $g, $b );
	$min = min( $r, $g, $b );
	$h   = 0;
	$s   = 0;
	$l   = ( $max + $min ) / 2;
	if ( $max !== $min ) {
		$d = $max - $min;
		$s = $l > 0.5 ? $d / ( 2 - $max - $min ) : $d / ( $max + $min );
		if ( $max === $r ) {
			$h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 );
		} elseif ( $max === $g ) {
			$h = ( $b - $r ) / $d + 2;
		} else {
			$h = ( $r - $g ) / $d + 4;
		}
		$h /= 6;
	}
	return array( $h, $s, $l );
}

/**
 * Get saturation (HSL) of a hex color.
 *
 * @param string $hex Hex color.
 * @return float Saturation in range 0..1.
 */
function thespl_saturation_hex( $hex ) {
	$rgb = thespl_hex_to_rgb( $hex );
	$hsl = thespl_rgb_to_hsl_components( $rgb[0], $rgb[1], $rgb[2] );
	return isset( $hsl[1] ) ? (float) $hsl[1] : 0.0;
}

/**
 * Convert HSL components to RGB.
 *
 * @param float $h Hue 0..1.
 * @param float $s Saturation 0..1.
 * @param float $l Lightness 0..1.
 * @return int[] Array of RGB components.
 */
function thespl_hsl_to_rgb_components( $h, $s, $l ) {
	$h = (float) $h;
	$s = (float) $s;
	$l = (float) $l;
	if ( $h < 0 ) {
		$h = 0.0;
	}
	if ( $h > 1 ) {
		$h = fmod( $h, 1.0 );
	}
	$s = max( 0.0, min( 1.0, $s ) );
	$l = max( 0.0, min( 1.0, $l ) );
	if ( $s <= 0 ) {
		$v = (int) round( $l * 255 );
		return array( $v, $v, $v );
	}
	$q          = ( $l < 0.5 ) ? ( $l * ( 1 + $s ) ) : ( $l + $s - ( $l * $s ) );
	$p          = ( 2 * $l ) - $q;
	$hue_to_rgb = function ( $p_c, $q_c, $t_c ) {
		if ( $t_c < 0 ) {
			++$t_c;
		}
		if ( $t_c > 1 ) {
			--$t_c;
		}
		if ( $t_c < ( 1 / 6 ) ) {
			return $p_c + ( ( $q_c - $p_c ) * 6 * $t_c );
		}
		if ( $t_c < ( 1 / 2 ) ) {
			return $q_c;
		}
		if ( $t_c < ( 2 / 3 ) ) {
			return $p_c + ( ( $q_c - $p_c ) * ( ( 2 / 3 ) - $t_c ) * 6 );
		}
		return $p_c;
	};
	$r          = $hue_to_rgb( $p, $q, $h + ( 1 / 3 ) );
	$g          = $hue_to_rgb( $p, $q, $h );
	$b          = $hue_to_rgb( $p, $q, $h - ( 1 / 3 ) );
	return array(
		(int) round( $r * 255 ),
		(int) round( $g * 255 ),
		(int) round( $b * 255 ),
	);
}

/**
 * Derive a Material-inspired accent color from base colors.
 *
 * @param string $bg_hex   Background hex color.
 * @param string $text_hex Text hex color.
 * @return string Accent color.
 */
function thespl_material_accent_from_base( $bg_hex, $text_hex ) {
	$bg_hex   = is_string( $bg_hex ) ? sanitize_hex_color( $bg_hex ) : '#111111';
	$text_hex = is_string( $text_hex ) ? sanitize_hex_color( $text_hex ) : '#eeeeee';
	if ( '' === $bg_hex ) {
		$bg_hex = '#111111';
	}
	if ( '' === $text_hex ) {
		$text_hex = thespl_best_text_on_bg( $bg_hex );
	}
	$rgb_source = thespl_hex_to_rgb( $text_hex );
	$hsl        = thespl_rgb_to_hsl_components( $rgb_source[0], $rgb_source[1], $rgb_source[2] );
	$h          = isset( $hsl[0] ) ? (float) $hsl[0] : 0.0;
	$s          = isset( $hsl[1] ) ? (float) $hsl[1] : 0.0;
	$l          = isset( $hsl[2] ) ? (float) $hsl[2] : 0.5;
	$s          = max( 0.35, min( 0.9, $s ) );
	$is_light   = thespl_luma_hex( $bg_hex ) > 0.5;
	$l          = $is_light ? 0.4 : 0.7;
	$rgb_accent = thespl_hsl_to_rgb_components( $h, $s, $l );
	$accent     = thespl_rgb_to_hex( $rgb_accent[0], $rgb_accent[1], $rgb_accent[2] );
	$accent     = sanitize_hex_color( $accent );
	if ( '' === $accent ) {
		$accent = thespl_pick_vivid_accent( $bg_hex );
	}
	return $accent;
}

/**
 * Pick a strong accent color from a palette based on contrast and saturation.
 *
 * @param array  $palette  Palette values.
 * @param string $bg_hex   Background color.
 * @param string $text_hex Text color.
 * @return string Accent color.
 */
function thespl_best_accent_from_palette( $palette, $bg_hex, $text_hex ) {
	$bg_hex     = is_string( $bg_hex ) ? sanitize_hex_color( $bg_hex ) : '#111111';
	$text_hex   = is_string( $text_hex ) ? sanitize_hex_color( $text_hex ) : '#eeeeee';
	$best       = '';
	$best_score = -1;
	if ( is_array( $palette ) ) {
		foreach ( $palette as $slug => $color ) {
			if ( ! is_string( $color ) ) {
				continue; }
			$hex = sanitize_hex_color( $color );
			if ( '' === $hex ) {
				continue; }
			$lower = strtolower( $hex );
			if ( strtolower( $bg_hex ) === $lower ) {
				continue; }
			if ( strtolower( $text_hex ) === $lower ) {
				continue; }
			$sat = thespl_saturation_hex( $hex );
			if ( $sat < 0.15 ) {
				continue; }
			$contrast_bg   = abs( thespl_luma_hex( $hex ) - thespl_luma_hex( $bg_hex ) );
			$contrast_text = abs( thespl_luma_hex( $hex ) - thespl_luma_hex( $text_hex ) );
			$score         = ( 0.6 * $contrast_bg ) + ( 0.2 * $contrast_text ) + ( 0.2 * $sat );
			if ( $score > $best_score ) {
				$best_score = $score;
				$best       = $hex;
			}
		}
	}
	if ( '' === $best ) {
		$best = thespl_pick_vivid_accent( $bg_hex );
	}
	return sanitize_hex_color( $best );
}

/**
 * Generate a palette for a playlist based on base colors.
 *
 * @param int   $playlist_id Playlist post ID.
 * @param array $hints       Optional hints (e.g. page_bg, page_text).
 * @return array{tokens:array,design:array,sources:array} Palette tokens, design meta and debug sources.
 */
function thespl_generate_magic_palette( $playlist_id, $hints = array() ) {
	$playlist_id       = (int) $playlist_id;
	$accent            = '';
	$accent2           = '';
	$bg                = '#111111';
	$text              = '';
	$waveform          = '';
	$debug             = array();
	$hints             = is_array( $hints ) ? $hints : array();
	$debug['pipeline'] = array();
	$inputs            = array();
	if ( isset( $hints['page_bg'] ) && is_string( $hints['page_bg'] ) ) {
		$inputs['page_bg'] = sanitize_hex_color( $hints['page_bg'] );
	}
	if ( isset( $hints['page_text'] ) && is_string( $hints['page_text'] ) ) {
		$inputs['page_text'] = sanitize_hex_color( $hints['page_text'] );
	}
	if ( isset( $hints['accent_1'] ) && is_string( $hints['accent_1'] ) ) {
		$inputs['accent_1'] = sanitize_hex_color( $hints['accent_1'] );
	}
	if ( isset( $hints['accent_2'] ) && is_string( $hints['accent_2'] ) ) {
		$inputs['accent_2'] = sanitize_hex_color( $hints['accent_2'] );
	}
	$debug['inputs'] = $inputs;
	$step            = array(
		'step'   => 'base_from_input',
		'status' => 'no_data',
	);
	if ( isset( $inputs['page_bg'] ) && '' !== $inputs['page_bg'] ) {
		$bg         = $inputs['page_bg'];
		$step['bg'] = $bg;
	}
	if ( isset( $inputs['page_text'] ) && '' !== $inputs['page_text'] ) {
		$text         = $inputs['page_text'];
		$step['text'] = $text;
	}
	if ( isset( $step['bg'] ) || isset( $step['text'] ) ) {
		$step['status'] = 'success';
	}
	$debug['pipeline'][] = $step;
	if ( '' === $bg || ! is_string( $bg ) ) {
		$bg = '#111111';
	}
	$bg = sanitize_hex_color( $bg );
	if ( '' === $bg ) {
		$bg = '#111111';
	}
	if ( '' === $text || ! is_string( $text ) ) {
		$text = thespl_best_text_on_bg( $bg );
	}
	$text = sanitize_hex_color( $text );
	if ( '' === $text ) {
		$text = thespl_best_text_on_bg( $bg );
	}
	$accent_step = array(
		'step'         => 'accent_from_input_or_material',
		'created_from' => '',
		'status'       => 'no_data',
	);
	if ( isset( $inputs['accent_1'] ) && '' !== $inputs['accent_1'] ) {
		$accent                      = $inputs['accent_1'];
		$accent_step['accent']       = $accent;
		$accent_step['created_from'] = 'input';
	}
	if ( isset( $inputs['accent_2'] ) && '' !== $inputs['accent_2'] ) {
		$accent2                     = $inputs['accent_2'];
		$accent_step['accent_2']     = $accent2;
		$accent_step['created_from'] = '' === $accent_step['created_from'] ? 'input' : $accent_step['created_from'];
	}
	if ( '' === $accent ) {
		$accent                      = thespl_material_accent_from_base( $bg, $text );
		$accent_step['accent']       = $accent;
		$accent_step['created_from'] = '' === $accent_step['created_from'] ? 'material' : $accent_step['created_from'];
	}
	if ( '' === $accent2 ) {
		$accent2                 = thespl_mix_hex( $accent, '#ffffff', 0.6 );
		$accent_step['accent_2'] = $accent2;
	}
	if ( '' !== $accent || '' !== $accent2 ) {
		$accent_step['status'] = 'success';
	}
	$debug['pipeline'][] = $accent_step;
	$contrast            = thespl_contrast_ratio_hex( $text, $bg );
	if ( $contrast < 4.5 ) {
		$debug['contrast'] = array(
			'text_original' => $text,
			'bg'            => $bg,
			'ratio'         => $contrast,
		);
		$text              = thespl_best_text_on_bg( $bg );
	}
	$ring_base      = $bg;
	$waveform       = ( thespl_luma_hex( $bg ) > 0.5 ) ? thespl_mix_hex( $bg, '#000000', 0.6 ) : thespl_mix_hex( $bg, '#ffffff', 0.5 );
	$bg_luma        = thespl_luma_hex( $bg );
	$primary_base   = $accent;
	$secondary_base = $accent2;
	if ( $bg_luma >= 0.5 ) {
		$primary             = thespl_mix_hex( $primary_base, '#000000', 0.25 );
		$primary_container   = thespl_mix_hex( $primary_base, '#000000', 0.1 );
		$secondary           = thespl_mix_hex( $secondary_base, '#000000', 0.25 );
		$secondary_container = thespl_mix_hex( $secondary_base, '#000000', 0.1 );
	} else {
		$primary             = thespl_mix_hex( $primary_base, '#ffffff', 0.4 );
		$primary_container   = thespl_mix_hex( $primary_base, '#ffffff', 0.25 );
		$secondary           = thespl_mix_hex( $secondary_base, '#ffffff', 0.4 );
		$secondary_container = thespl_mix_hex( $secondary_base, '#ffffff', 0.25 );
	}
	$accent2_contrast = thespl_contrast_ratio_hex( $secondary, $bg );
	if ( $accent2_contrast < 2.0 ) {
		if ( $bg_luma >= 0.5 ) {
			$secondary = thespl_mix_hex( $secondary, '#000000', 0.35 );
		} else {
			$secondary = thespl_mix_hex( $secondary, '#ffffff', 0.5 );
		}
	}
	$primary = sanitize_hex_color( $primary );
	if ( '' === $primary ) {
		$primary = $primary_base;
	}
	$primary_container = sanitize_hex_color( $primary_container );
	if ( '' === $primary_container ) {
		$primary_container = $primary_base;
	}
	$secondary = sanitize_hex_color( $secondary );
	if ( '' === $secondary ) {
		$secondary = $secondary_base;
	}
	$secondary_container = sanitize_hex_color( $secondary_container );
	if ( '' === $secondary_container ) {
		$secondary_container = $secondary_base;
	}
	$on_surface             = $text;
	$on_primary             = thespl_best_text_on_bg( $primary );
	$on_primary_container   = thespl_best_text_on_bg( $primary_container );
	$on_secondary           = thespl_best_text_on_bg( $secondary );
	$on_secondary_container = thespl_best_text_on_bg( $secondary_container );
	$text_rgb               = thespl_hex_to_rgb( $on_surface );
	$accent                 = $primary;
	$accent2                = $secondary;
	$accent_rgb             = thespl_hex_to_rgb( $accent );
	$chip_base_hex          = $secondary_container ? $secondary_container : $primary_container;
	$chip_base_hex          = sanitize_hex_color( $chip_base_hex );
	if ( '' === $chip_base_hex ) {
		$chip_base_hex = $primary_container;
	}
	$chip_base_rgb = thespl_hex_to_rgb( $chip_base_hex );
	if ( ! is_array( $chip_base_rgb ) ) {
		$chip_base_rgb = $text_rgb;
	}
	$tokens                             = array(
		'accent'             => $accent,
		'accent_2'           => $accent2,
		'bg'                 => $bg,
		'text'               => $on_surface,
		'muted'              => sprintf( 'rgba(%d,%d,%d,0.7)', $text_rgb[0], $text_rgb[1], $text_rgb[2] ),
		'waveform'           => $waveform,
		'ring_base'          => $ring_base,
		'playhead_bg'        => thespl_mix_hex( $bg, '#ffffff', 0.1 ),
		'playhead_fill'      => $primary,
		'button_bg'          => $primary_container,
		'button_text'        => thespl_best_text_on_bg( $primary_container ),
		'button_active_bg'   => $primary,
		'button_active_text' => thespl_best_text_on_bg( $primary ),
		'chip_bg'            => sprintf( 'rgba(%d,%d,%d,0.18)', $chip_base_rgb[0], $chip_base_rgb[1], $chip_base_rgb[2] ),
		'chip_br'            => sprintf( 'rgba(%d,%d,%d,0.35)', $chip_base_rgb[0], $chip_base_rgb[1], $chip_base_rgb[2] ),
		'chip_selected_bg'   => $secondary,
		'chip_selected_br'   => sprintf( 'rgba(%d,%d,%d,0.7)', $accent_rgb[0], $accent_rgb[1], $accent_rgb[2] ),
		'panel_bg'           => sprintf( 'rgba(%d,%d,%d,0.05)', $text_rgb[0], $text_rgb[1], $text_rgb[2] ),
		'panel_br'           => sprintf( 'rgba(%d,%d,%d,0.12)', $text_rgb[0], $text_rgb[1], $text_rgb[2] ),
	);
	$debug['roles']                     = array(
		'surface'                => $bg,
		'on_surface'             => $on_surface,
		'primary'                => $primary,
		'on_primary'             => $on_primary,
		'primary_container'      => $primary_container,
		'on_primary_container'   => $on_primary_container,
		'secondary'              => $secondary,
		'on_secondary'           => $on_secondary,
		'secondary_container'    => $secondary_container,
		'on_secondary_container' => $on_secondary_container,
	);
	$debug['resolved']                  = array(
		'accent'   => $accent,
		'accent_2' => $accent2,
		'bg'       => $bg,
		'text'     => $on_surface,
		'waveform' => $waveform,
	);
	$design                             = get_post_meta( $playlist_id, '_thespl_playlist_design', true );
	$design                             = is_array( $design ) ? $design : array();
	$design['accent_color']             = $tokens['accent'];
	$design['accent_2_color']           = $tokens['accent_2'];
	$design['base_color']               = $tokens['bg'];
	$design['text_color']               = $tokens['text'];
	$design['waveform_color']           = $tokens['waveform'];
	$design['ring_base_color']          = $tokens['ring_base'];
	$design['button_bg_color']          = $tokens['button_bg'];
	$design['button_text_color']        = $tokens['button_text'];
	$design['button_active_bg_color']   = $tokens['button_active_bg'];
	$design['button_active_text_color'] = $tokens['button_active_text'];
	$design['chip_bg_color']            = $tokens['chip_bg'];
	$design['chip_br_color']            = $tokens['chip_br'];
	return array(
		'tokens'  => $tokens,
		'design'  => $design,
		'sources' => $debug,
	);
}

/**
 * REST: Generate a magic palette (optionally applying it).
 *
 * @param WP_REST_Request $request Request.
 * @return WP_REST_Response|WP_Error
 */
function thespl_rest_magic_palette( WP_REST_Request $request ) {
	$id      = (int) $request->get_param( 'playlist_id' );
	$apply   = (bool) $request->get_param( 'apply' );
	$page_bg = $request->get_param( 'page_bg' );
	$page_tx = $request->get_param( 'page_text' );
	$acc1    = $request->get_param( 'accent_1' );
	$acc2    = $request->get_param( 'accent_2' );
	if ( $id <= 0 || ! current_user_can( 'edit_post', $id ) ) {
		return new WP_Error( 'thespl_forbidden', __( 'Forbidden', 'the-studio-player' ), array( 'status' => 403 ) );
	}
	$hints = array();
	if ( is_string( $page_bg ) ) {
		$hex = sanitize_hex_color( $page_bg );
		if ( '' !== $hex ) {
			$hints['page_bg'] = $hex;
		}
	}
	if ( is_string( $page_tx ) ) {
		$hex_t = sanitize_hex_color( $page_tx );
		if ( '' !== $hex_t ) {
			$hints['page_text'] = $hex_t;
		}
	}
	if ( is_string( $acc1 ) ) {
		$hex_a1 = sanitize_hex_color( $acc1 );
		if ( '' !== $hex_a1 ) {
			$hints['accent_1'] = $hex_a1;
		}
	}
	if ( is_string( $acc2 ) ) {
		$hex_a2 = sanitize_hex_color( $acc2 );
		if ( '' !== $hex_a2 ) {
			$hints['accent_2'] = $hex_a2;
		}
	}
	$res = thespl_generate_magic_palette( $id, $hints );
	if ( $apply ) {
		update_post_meta( $id, '_thespl_playlist_design', $res['design'] );
	}
	return rest_ensure_response( $res );
}

/**
 * Pick a vivid fallback accent color with good contrast to a background.
 *
 * @param string $bg_hex Background color.
 * @return string Accent color.
 */
function thespl_pick_vivid_accent( $bg_hex ) {
	$candidates = array( '#ef4444', '#22c55e', '#3b82f6', '#8b5cf6', '#f59e0b', '#10b981', '#e11d48' );
	$best       = '#22d3ee';
	$best_diff  = -1;
	foreach ( $candidates as $hex ) {
		$diff = abs( thespl_luma_hex( $hex ) - thespl_luma_hex( $bg_hex ) );
		if ( $diff > $best_diff ) {
			$best_diff = $diff;
			$best      = $hex;
		}
	}
	return sanitize_hex_color( $best );
}

/**
 * Read the leading bytes of a local attachment file.
 *
 * Uses a direct local file handle because WP_Filesystem has no seek-based
 * partial-read API and get_contents() would load the full file into memory.
 *
 * @param string $file_path Absolute local file path.
 * @param int    $length    Number of bytes to read from the start of the file.
 * @return string
 */
function thespl_read_local_attachment_header( $file_path, $length ) {
	$length = (int) $length;
	if ( ! is_string( $file_path ) || '' === $file_path || $length <= 0 ) {
		return '';
	}
	if ( ! is_file( $file_path ) || ! is_readable( $file_path ) ) {
		return '';
	}

	// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fread, WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- WP_Filesystem has no partial-read API for local attachment headers.
	$handle = fopen( $file_path, 'rb' );
	if ( false === $handle ) {
		return '';
	}

	$header    = '';
	$remaining = $length;
	while ( $remaining > 0 && ! feof( $handle ) ) {
		$chunk = fread( $handle, min( 8192, $remaining ) );
		if ( false === $chunk ) {
			$header = '';
			break;
		}
		if ( '' === $chunk ) {
			break;
		}
		$header    .= $chunk;
		$remaining -= strlen( $chunk );
	}

	fclose( $handle );
	// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fread, WordPress.WP.AlternativeFunctions.file_system_operations_fclose

	return $header;
}

/**
 * Read audio technical metadata from an attachment (best effort).
 *
 * @param int $attachment_id Attachment ID.
 * @return array
 */
function thespl_get_audio_tech_meta( $attachment_id ) {
	$attachment_id = (int) $attachment_id;
	if ( $attachment_id <= 0 ) {
		return array(
			'channels'        => 0,
			'sample_rate'     => 0,
			'bits_per_sample' => 0,
			'bitrate'         => 0,
		);
	}

	$file_path = get_attached_file( $attachment_id );
	if ( ! is_string( $file_path ) || '' === $file_path ) {
		return array(
			'channels'        => 0,
			'sample_rate'     => 0,
			'bits_per_sample' => 0,
			'bitrate'         => 0,
		);
	}

	$ext = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
	if ( 'flac' === $ext ) {
		$buf = thespl_read_local_attachment_header( $file_path, 42 );
		if ( ! is_string( $buf ) || '' === $buf ) {
			return array(
				'channels'        => 0,
				'sample_rate'     => 0,
				'bits_per_sample' => 0,
				'bitrate'         => 0,
			);
		}
		$magic = substr( $buf, 0, 4 );
		if ( 'fLaC' !== $magic ) {
			return array(
				'channels'        => 0,
				'sample_rate'     => 0,
				'bits_per_sample' => 0,
				'bitrate'         => 0,
			);
		}
		$hdr = substr( $buf, 4, 4 );
		if ( ! is_string( $hdr ) || 4 !== strlen( $hdr ) ) {
			return array(
				'channels'        => 0,
				'sample_rate'     => 0,
				'bits_per_sample' => 0,
				'bitrate'         => 0,
			);
		}
		$hdr_u      = unpack( 'N', $hdr );
		$hdr_val    = is_array( $hdr_u ) && isset( $hdr_u[1] ) ? (int) $hdr_u[1] : 0;
		$block_type = ( $hdr_val >> 24 ) & 0x7F;
		$block_len  = $hdr_val & 0xFFFFFF;
		if ( 0 !== $block_type || $block_len < 34 ) {
			return array(
				'channels'        => 0,
				'sample_rate'     => 0,
				'bits_per_sample' => 0,
				'bitrate'         => 0,
			);
		}
		$streaminfo = substr( $buf, 8, 34 );
		if ( ! is_string( $streaminfo ) || 34 !== strlen( $streaminfo ) ) {
			return array(
				'channels'        => 0,
				'sample_rate'     => 0,
				'bits_per_sample' => 0,
				'bitrate'         => 0,
			);
		}
		$b = unpack( 'C*', $streaminfo );
		if ( ! is_array( $b ) || ! isset( $b[11], $b[12], $b[13], $b[14] ) ) {
			return array(
				'channels'        => 0,
				'sample_rate'     => 0,
				'bits_per_sample' => 0,
				'bitrate'         => 0,
			);
		}
		$b10             = (int) $b[11];
		$b11             = (int) $b[12];
		$b12             = (int) $b[13];
		$b13             = (int) $b[14];
		$sample_rate     = ( $b10 << 12 ) | ( $b11 << 4 ) | ( ( $b12 & 0xF0 ) >> 4 );
		$channels        = ( ( ( $b12 & 0x0E ) >> 1 ) + 1 );
		$bits_per_sample = ( ( ( ( $b12 & 0x01 ) << 4 ) | ( ( $b13 & 0xF0 ) >> 4 ) ) + 1 );
		return array(
			'channels'        => (int) $channels,
			'sample_rate'     => (int) $sample_rate,
			'bits_per_sample' => (int) $bits_per_sample,
			'bitrate'         => 0,
		);
	}

	$meta = wp_get_attachment_metadata( $attachment_id );
	if ( is_array( $meta ) ) {
		$audio    = isset( $meta['audio'] ) && is_array( $meta['audio'] ) ? $meta['audio'] : array();
		$channels = 0;
		if ( isset( $meta['channels'] ) ) {
			$channels = (int) $meta['channels'];
		} elseif ( isset( $audio['channels'] ) ) {
			$channels = (int) $audio['channels'];
		}
		$sample_rate = 0;
		if ( isset( $meta['sample_rate'] ) ) {
			$sample_rate = (int) $meta['sample_rate'];
		} elseif ( isset( $audio['sample_rate'] ) ) {
			$sample_rate = (int) $audio['sample_rate'];
		}
		$bits_per_sample = 0;
		if ( isset( $meta['bits_per_sample'] ) ) {
			$bits_per_sample = (int) $meta['bits_per_sample'];
		} elseif ( isset( $audio['bits_per_sample'] ) ) {
			$bits_per_sample = (int) $audio['bits_per_sample'];
		}
		$bitrate = 0;
		if ( isset( $meta['bitrate'] ) ) {
			$bitrate = (int) $meta['bitrate'];
		} elseif ( isset( $audio['bitrate'] ) ) {
			$bitrate = (int) $audio['bitrate'];
		}
		return array(
			'channels'        => $channels,
			'sample_rate'     => $sample_rate,
			'bits_per_sample' => $bits_per_sample,
			'bitrate'         => $bitrate,
		);
	}
	return array(
		'channels'        => 0,
		'sample_rate'     => 0,
		'bits_per_sample' => 0,
		'bitrate'         => 0,
	);
}

/**
 * Convert an absolute upload path to a relative path within uploads.
 *
 * @param string $abs_path Absolute path.
 * @return string Relative path or empty string.
 */
function thespl_get_upload_relative_path( $abs_path ) {
	if ( ! is_string( $abs_path ) || '' === $abs_path ) {
		return '';
	}
	$uploads = wp_get_upload_dir();
	$basedir = isset( $uploads['basedir'] ) ? realpath( $uploads['basedir'] ) : false;
	if ( ! $basedir ) {
		return '';
	}
	$basedir_norm = trailingslashit( wp_normalize_path( $basedir ) );
	$abs_norm     = wp_normalize_path( (string) $abs_path );
	if ( '' === $basedir_norm || '' === $abs_norm || 0 !== strpos( $abs_norm, $basedir_norm ) ) {
		return '';
	}
	return ltrim( substr( $abs_norm, strlen( $basedir_norm ) ), '/' );
}

/**
 * Build a public URL from an uploads-relative path and baseurl.
 *
 * @param string $rel_path Relative path under uploads.
 * @param string $baseurl  Uploads base URL.
 * @return string
 */
function thespl_build_upload_url( $rel_path, $baseurl ) {
	$rel_path = is_string( $rel_path ) ? trim( $rel_path ) : '';
	$baseurl  = is_string( $baseurl ) ? trim( $baseurl ) : '';
	if ( '' === $rel_path || '' === $baseurl ) {
		return '';
	}
	return trailingslashit( $baseurl ) . str_replace( '%2F', '/', rawurlencode( str_replace( '\\', '/', $rel_path ) ) );
}

/**
 * Find an attachment ID by its _wp_attached_file relative path.
 *
 * @param string $relative_path Relative file path in uploads.
 * @return int Attachment ID or 0.
 */
function thespl_find_attachment_id_by_uploaded_file( $relative_path ) {
	$relative_path = is_string( $relative_path ) ? trim( $relative_path ) : '';
	if ( '' === $relative_path ) {
		return 0;
	}
	$cache_key   = 'thespl_attach_by_file_' . md5( $relative_path );
	$cache_group = 'the_studio_player';
	$cached      = wp_cache_get( $cache_key, $cache_group );
	if ( false !== $cached ) {
		return (int) $cached;
	}
	$uploads   = wp_get_upload_dir();
	$baseurl   = isset( $uploads['baseurl'] ) ? (string) $uploads['baseurl'] : '';
	$rel_norm  = ltrim( str_replace( '\\', '/', $relative_path ), '/' );
	$full_url  = '' !== $baseurl ? trailingslashit( $baseurl ) . $rel_norm : '';
	$attach_id = '' !== $full_url ? (int) attachment_url_to_postid( $full_url ) : 0;
	wp_cache_set( $cache_key, $attach_id, $cache_group, 300 );
	return $attach_id;
}

/**
 * Guess related media (cover, waveform JSON) for an audio file.
 *
 * @param int $audio_attachment_id Audio attachment ID.
 * @return array Map with optional 'cover' and 'waveform'.
 */
function thespl_guess_related_media_for_audio( $audio_attachment_id ) {
	$audio_attachment_id = (int) $audio_attachment_id;
	if ( $audio_attachment_id <= 0 ) {
		return array();
	}
	$audio_path = get_attached_file( $audio_attachment_id );
	if ( ! is_string( $audio_path ) || '' === $audio_path ) {
		return array();
	}
	$uploads = wp_get_upload_dir();
	$basedir = isset( $uploads['basedir'] ) ? realpath( $uploads['basedir'] ) : false;
	$baseurl = isset( $uploads['baseurl'] ) ? (string) $uploads['baseurl'] : '';
	if ( ! $basedir || '' === $baseurl ) {
		return array();
	}
	$audio_real = realpath( $audio_path );
	if ( ! $audio_real ) {
		return array();
	}
	$dir  = dirname( $audio_real );
	$base = pathinfo( $audio_real, PATHINFO_FILENAME );
	if ( '' === $base ) {
		return array();
	}
	$out       = array();
	$json_path = $dir . DIRECTORY_SEPARATOR . $base . '.json';
	if ( file_exists( $json_path ) ) {
		$rel             = thespl_get_upload_relative_path( $json_path );
		$attach_id       = '' !== $rel ? thespl_find_attachment_id_by_uploaded_file( $rel ) : 0;
		$url             = $attach_id > 0 ? wp_get_attachment_url( $attach_id ) : thespl_build_upload_url( $rel, $baseurl );
		$out['waveform'] = array(
			'attachment_id' => $attach_id,
			'url'           => $url ? (string) $url : '',
		);
	}
	$img_exts = array( 'jpg', 'jpeg', 'png', 'webp' );
	foreach ( $img_exts as $ext ) {
		$img_path = $dir . DIRECTORY_SEPARATOR . $base . '.' . $ext;
		if ( ! file_exists( $img_path ) ) {
			continue;
		}
		$rel       = thespl_get_upload_relative_path( $img_path );
		$attach_id = '' !== $rel ? thespl_find_attachment_id_by_uploaded_file( $rel ) : 0;
		$url       = '';
		if ( $attach_id > 0 ) {
			$url = (string) wp_get_attachment_image_url( $attach_id, 'medium' );
		}
		if ( '' === $url ) {
			$url = thespl_build_upload_url( $rel, $baseurl );
		}
		$out['cover'] = array(
			'attachment_id' => $attach_id,
			'url'           => $url ? (string) $url : '',
		);
		break;
	}
	return $out;
}

/**
 * Enqueue player assets and expose configuration.
 *
 * @return void
 */
function thespl_player_enqueue_assets() {
	$version       = THESPL_VERSION;
	$core_js_path  = THESPL_PLUGIN_DIR . 'assets/js/player-core.js';
	$core_js_mtime = 0;
	if ( file_exists( $core_js_path ) ) {
		$core_js_mtime = (int) filemtime( $core_js_path );
	}
	$build_id            = substr( hash( 'sha256', $version . '|' . (string) $core_js_mtime ), 0, 10 );
	$tpl_slug            = isset( $GLOBALS['thespl_current_template_slug'] ) ? sanitize_key( $GLOBALS['thespl_current_template_slug'] ) : '';
	$tpl_slug_for_assets = $tpl_slug;
	if ( ! thespl_template_slug_is_valid( $tpl_slug ) ) {
		$tpl_slug_for_assets = 'default';
	}
	$base_style_handle = '';
	$tpl_style_handle  = '';
	$asset_base        = thespl_get_player_template_asset_base( $tpl_slug_for_assets );
	$tpl_style_path    = $asset_base['dir'] . 'style.css';
	if ( file_exists( $tpl_style_path ) ) {
		$tpl_style_handle = 'thespl-player-template-' . $tpl_slug_for_assets;
		$deps             = '' !== $base_style_handle ? array( $base_style_handle ) : array();
		$tpl_style_url    = $asset_base['url'] . 'style.css';
		wp_enqueue_style( $tpl_style_handle, $tpl_style_url, $deps, $version );
	}
	$player_ui_style_deps = array();
	if ( '' !== $tpl_style_handle ) {
		$player_ui_style_deps[] = $tpl_style_handle;
	} elseif ( '' !== $base_style_handle ) {
		$player_ui_style_deps[] = $base_style_handle;
	}
	wp_enqueue_style( 'thespl-player-ui', THESPL_PLUGIN_URL . 'assets/css/player-ui.css', $player_ui_style_deps, $version );
	wp_enqueue_script( 'thespl-player-core', THESPL_PLUGIN_URL . 'assets/js/player-core.js', array(), $version . '-' . $build_id, true );
	wp_enqueue_script( 'thespl-player-store', THESPL_PLUGIN_URL . 'assets/js/player-store.js', array( 'thespl-player-core' ), $version . '-' . $build_id, true );
	wp_enqueue_script( 'thespl-player-ui', THESPL_PLUGIN_URL . 'assets/js/player-ui.js', array( 'thespl-player-store' ), $version . '-' . $build_id, true );
	$tpl_script_path = $asset_base['dir'] . 'script.js';
	if ( file_exists( $tpl_script_path ) ) {
		$tpl_handle = 'thespl-player-template-' . $tpl_slug_for_assets;
		wp_enqueue_script( $tpl_handle, $asset_base['url'] . 'script.js', array( 'thespl-player-core', 'thespl-player-store', 'thespl-player-ui' ), $version . '-' . $build_id, true );
	}
	$data            = array(
		'restUrl'         => esc_url_raw( rest_url( 'thespl/v1' ) ),
		'restNonce'       => wp_create_nonce( 'wp_rest' ),
		'defaultCoverUrl' => esc_url_raw( THESPL_PLUGIN_URL . 'assets/img/default-cover.svg' ),
		'plugin'          => array(
			'version'  => (string) $version,
			'codename' => defined( 'THESPL_CODENAME' ) ? (string) THESPL_CODENAME : '',
			'build'    => $build_id,
		),
		'l10n'            => array(
			'buffering'                         => __( 'Buffering', 'the-studio-player' ),
			'ready'                             => __( 'Ready', 'the-studio-player' ),
			'tokenExpired'                      => __( 'Security Token Expired - reload page', 'the-studio-player' ),
			'mediaMissing'                      => __( 'Media Missing!', 'the-studio-player' ),
			'tooManyRequests'                   => __( 'Too many requests - slow down', 'the-studio-player' ),
			'streamingError'                    => __( 'Streaming Error', 'the-studio-player' ),
			'error-streaming'                   => __( 'Streaming error. Please try again.', 'the-studio-player' ),
			'error-streaming-http'              => __( 'Streaming error (network or server issue). Please try again.', 'the-studio-player' ),
			'error-track-unplayable'            => __( 'This track cannot be played.', 'the-studio-player' ),
			'error-media-file-not-found'        => __( 'Media file not found (404).', 'the-studio-player' ),
			'error-media-file-not-found-global' => __( 'Media file(s) not found (404).', 'the-studio-player' ),
			'error-waveform-not-found'          => __( 'Waveform file not found (404).', 'the-studio-player' ),
			'error-no-playable-tracks'          => __( 'No playable tracks are available in this playlist.', 'the-studio-player' ),
			'error-playlist-load'               => __( 'Unable to load playlist. Please try again later.', 'the-studio-player' ),
			'error-playlist-not-found'          => __( 'Playlist not found.', 'the-studio-player' ),
			'error-token-expired'               => __( 'Your session has expired. Please reload the page.', 'the-studio-player' ),
			'error-auth'                        => __( 'Authorization error. Please check your access or reload the page.', 'the-studio-player' ),
			'error-security-throttled'          => __( 'Too many requests. Please wait a moment and try again.', 'the-studio-player' ),
			'error-network-limited'             => __( 'Your network connection is unstable. Playback was adjusted.', 'the-studio-player' ),
			'error-audio-context'               => __( 'Audio playback is not available in this browser.', 'the-studio-player' ),
			'error-autoplay-blocked'            => __( 'Autoplay was blocked by your browser.', 'the-studio-player' ),
			'error-internal'                    => __( 'Unexpected player error. Please contact the site administrator or plugin developer.', 'the-studio-player' ),
		),
	);
	$player_settings = thespl_get_player_settings();
	$fade_ms         = isset( $player_settings['fade_switch_duration_ms'] ) ? (int) $player_settings['fade_switch_duration_ms'] : 1;
	if ( $fade_ms < 0 ) {
		$fade_ms = 0; }
	if ( $fade_ms > 50 ) {
		$fade_ms = 50; }
	$data['fadeSwitchDurationMs'] = $fade_ms;
	$chunk_level                  = isset( $player_settings['streaming_chunk_level'] ) ? (int) $player_settings['streaming_chunk_level'] : 4;
	if ( $chunk_level < 0 ) {
		$chunk_level = 0; }
	if ( $chunk_level > 9 ) {
		$chunk_level = 9; }
	$prebuffer_sec = isset( $player_settings['streaming_prebuffer_sec'] ) ? (int) $player_settings['streaming_prebuffer_sec'] : 0;
	if ( $prebuffer_sec < 0 ) {
		$prebuffer_sec = 0; }
	if ( $prebuffer_sec > 30 ) {
		$prebuffer_sec = 30; }
	$auto_detect            = ! empty( $player_settings['streaming_auto_detect_network'] );
	$continue_on_pause      = ! empty( $player_settings['streaming_continue_buffer_on_pause'] );
	$prefetch_next_on_ahead = ! empty( $player_settings['streaming_prefetch_next_on_ahead'] );
	$ready_ahead_sec        = isset( $player_settings['streaming_ready_ahead_sec'] ) ? (int) $player_settings['streaming_ready_ahead_sec'] : 1;
	if ( $ready_ahead_sec < 1 ) {
		$ready_ahead_sec = 1; }
	if ( $ready_ahead_sec > 10 ) {
		$ready_ahead_sec = 10; }
	$data['streaming'] = array(
		'chunkSizeLevel'        => $chunk_level,
		'prebufferSec'          => $prebuffer_sec,
		'autoDetect'            => (bool) $auto_detect,
		'continueBufferOnPause' => (bool) $continue_on_pause,
		'prefetchNextOnAhead'   => (bool) $prefetch_next_on_ahead,
		'readyAheadSec'         => $ready_ahead_sec,
	);
	$debug_enabled     = ! empty( $player_settings['debug_console_enabled'] );
	$data['debug']     = (bool) $debug_enabled;
	wp_add_inline_script( 'thespl-player-core', 'window.thesplPlayer=' . wp_json_encode( $data ) . ';', 'before' );
}

/**
 * Conditionally enqueue player assets for content containing player.
 *
 * @return void
 */
function thespl_maybe_enqueue_player_assets() {
	if ( is_admin() ) {
		return;
	}
	if ( ! is_singular() ) {
		return;
	}
	global $post;
	if ( ! $post || ! isset( $post->post_content ) || ! is_string( $post->post_content ) ) {
		return;
	}
	if ( has_shortcode( $post->post_content, 'thespl_player' ) ) {
		thespl_player_enqueue_assets();
	}
}
add_action( 'wp_enqueue_scripts', 'thespl_maybe_enqueue_player_assets' );

/**
 * Shortcode renderer for the player.
 *
 * @param array $atts Shortcode attributes.
 * @return string
 */
function thespl_player_shortcode( $atts ) {
	$atts        = shortcode_atts(
		array(
			'playlist_id' => 0,
			'id'          => 0,
			'template'    => '',
		),
		$atts,
		'thespl_player'
	);
	$playlist_id = (int) $atts['playlist_id'];
	if ( $playlist_id <= 0 ) {
		$playlist_id = (int) $atts['id'];
	}
	if ( $playlist_id <= 0 ) {
		if ( current_user_can( 'manage_options' ) ) {
			$example = thespl_get_player_shortcode_for_playlist( 123, function_exists( 'thespl_get_default_template_slug' ) ? thespl_get_default_template_slug() : '' );
			if ( '' === $example ) {
				$example = '[thespl_player playlist_id="123"]';
			}
			$html  = '<div class="thespl-player">';
			$html .= '<div>' . esc_html__( 'The Studio Player: missing or invalid playlist id in shortcode.', 'the-studio-player' ) . '</div>';
			$html .= '<div><code>' . esc_html( $example ) . '</code></div>';
			$html .= '</div>';
			return $html;
		}
		return '';
	}
	$playlist_post = get_post( $playlist_id );
	$debug_message = '';
	if ( ! $playlist_post || 'thespl_playlist' !== $playlist_post->post_type ) {
		$debug_message = esc_html__( 'The Studio Player: playlist id does not exist or is not a THESPL playlist.', 'the-studio-player' );
	} elseif ( 'publish' !== $playlist_post->post_status ) {
		$debug_message = esc_html__( 'The Studio Player: playlist is not published.', 'the-studio-player' );
	}
	if ( '' !== $debug_message ) {
		return function_exists( 'thespl_render_player_unavailable_notice' ) ? thespl_render_player_unavailable_notice( $debug_message ) : '<div class="thespl-player"><div class="thespl-player__notice" role="status">' . esc_html( $debug_message ) . '</div></div>';
	}
	$template_slug = isset( $atts['template'] ) ? sanitize_key( $atts['template'] ) : '';
	if ( '' === $template_slug && function_exists( 'thespl_get_default_template_slug' ) ) {
		$template_slug = thespl_get_default_template_slug();
	}
	$GLOBALS['thespl_current_template_slug'] = $template_slug;
	thespl_player_enqueue_assets();
	return thespl_render_player_template(
		$template_slug,
		array(
			'playlist_id'   => $playlist_id,
			'template'      => $template_slug,
			'debug_message' => '',
		)
	);
}
add_shortcode( 'thespl_player', 'thespl_player_shortcode' );
