<?php
/**
 * Admin Helper Functions.
 *
 * @package TheStudioPlayer
 */

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

/**
 * Sanitize a subdirectory path.
 *
 * @param string $path Raw path.
 * @return string Sanitized path.
 */
function thespl_sanitize_subdir_path( $path ) {
	// Normalize backslashes to slashes.
	$path        = str_replace( '\\', '/', $path );
	$parts       = explode( '/', $path );
	$clean_parts = array();

	foreach ( $parts as $part ) {
		// sanitize_file_name removes special chars like ? & % : etc.
		$clean = sanitize_file_name( $part );

		// Explicitly block traversal dots.
		if ( ! empty( $clean ) && '.' !== $clean && '..' !== $clean ) {
			$clean_parts[] = $clean;
		}
	}

	return implode( '/', $clean_parts );
}

/**
 * Initialize WP_Filesystem.
 *
 * @return bool
 */
function thespl_init_filesystem() {
	global $wp_filesystem;
	if ( ! empty( $wp_filesystem ) ) {
		return true;
	}

	/*
	 * We explicitly load the core file-handling helpers here to bootstrap WP_Filesystem().
	 * This follows the WordPress.org recommended pattern: load the helper via require_once
	 * inside a function and call WP_Filesystem() immediately afterwards. The file is never
	 * used as a directly callable entry point and remains an internal implementation detail.
	 */
	require_once ABSPATH . 'wp-admin/includes/file.php';
	if ( WP_Filesystem() ) {
		return true;
	}
	return false;
}

/**
 * Resolve capability for tools policies based on plugin access baseline.
 *
 * @return string Capability for tools access.
 */
function thespl_get_tools_policy_capability() {
	$min_capability = function_exists( 'thespl_get_min_capability' ) ? thespl_get_min_capability() : 'edit_others_posts';
	$capability     = 'edit_others_posts';

	/*
	 * When plugin access is locked to administrators, tools must follow that
	 * stricter boundary. Otherwise tools stay Editor/Admin only.
	 */
	if ( 'manage_options' === $min_capability ) {
		$capability = 'manage_options';
	}

	/**
	 * Filter capability used for tools policies.
	 *
	 * @param string $capability     Resolved tools capability.
	 * @param string $min_capability Current plugin min capability.
	 */
	return (string) apply_filters( 'thespl_tools_policy_capability', $capability, $min_capability );
}

/**
 * Get the centralized policy map for admin permissions.
 *
 * @return array<string, array<string, mixed>> Policy definitions keyed by policy key.
 */
function thespl_get_policy_map() {
	$map = array(
		'plugin_access'        => array(
			'capability' => thespl_get_min_capability(),
		),
		'playlist_edit_own'    => array(
			'capability'         => thespl_get_min_capability(),
			'object_cap'         => 'edit_post',
			'requires_object_id' => true,
		),
		'playlist_edit_others' => array(
			'capability' => thespl_get_min_capability(),
			'object_cap' => 'edit_post',
		),
		'tools_diagnose'       => array(
			'capability' => thespl_get_tools_policy_capability(),
		),
		'tools_repair'         => array(
			'capability' => thespl_get_tools_policy_capability(),
		),
		'settings_general'     => array(
			'capability' => 'edit_others_posts',
		),
		'settings_security'    => array(
			'capability' => 'manage_options',
		),
		'license_view'         => array(
			'capability' => thespl_get_min_capability(),
		),
		'license_manage'       => array(
			'capability' => 'manage_options',
		),
	);

	/**
	 * Filter the centralized policy map.
	 *
	 * @param array<string, array<string, mixed>> $map Policy map.
	 */
	return (array) apply_filters( 'thespl_policy_map', $map );
}

/**
 * Resolve the base capability for a policy key.
 *
 * @param string $policy_key Policy key.
 * @return string Capability name, or empty string if the policy is unknown.
 */
function thespl_get_policy_capability( $policy_key ) {
	$policy_key = is_string( $policy_key ) ? sanitize_key( $policy_key ) : '';
	if ( '' === $policy_key ) {
		return '';
	}

	$map = thespl_get_policy_map();
	if ( ! isset( $map[ $policy_key ] ) || ! is_array( $map[ $policy_key ] ) ) {
		return '';
	}

	$definition = $map[ $policy_key ];
	$capability = isset( $definition['capability'] ) && is_string( $definition['capability'] ) ? $definition['capability'] : '';
	if ( '' === $capability ) {
		return '';
	}

	/**
	 * Filter the resolved policy capability.
	 *
	 * @param string $capability Resolved capability.
	 * @param string $policy_key Policy key.
	 * @param array  $context    Evaluation context.
	 */
	$capability = (string) apply_filters( 'thespl_policy_capability', $capability, $policy_key, array() );

	return $capability;
}

/**
 * Check whether current user can access a given policy.
 *
 * @param string $policy_key Policy key.
 * @param array  $context    Optional context data. Supported: object_id.
 * @return bool True when access is granted.
 */
function thespl_current_user_can_policy( $policy_key, $context = array() ) {
	$policy_key = is_string( $policy_key ) ? sanitize_key( $policy_key ) : '';
	if ( '' === $policy_key ) {
		return false;
	}

	$context = is_array( $context ) ? $context : array();
	$map     = thespl_get_policy_map();
	if ( ! isset( $map[ $policy_key ] ) || ! is_array( $map[ $policy_key ] ) ) {
		return false;
	}

	$definition = $map[ $policy_key ];
	$capability = isset( $definition['capability'] ) && is_string( $definition['capability'] ) ? $definition['capability'] : '';
	if ( '' === $capability ) {
		return false;
	}

	/**
	 * Filter the resolved policy capability.
	 *
	 * @param string $capability Resolved capability.
	 * @param string $policy_key Policy key.
	 * @param array  $context    Evaluation context.
	 */
	$capability = (string) apply_filters( 'thespl_policy_capability', $capability, $policy_key, $context );
	if ( '' === $capability || ! current_user_can( $capability ) ) {
		return false;
	}

	$requires_object_id = ! empty( $definition['requires_object_id'] );
	$object_cap         = isset( $definition['object_cap'] ) && is_string( $definition['object_cap'] ) ? $definition['object_cap'] : '';
	$has_object_id      = isset( $context['object_id'] ) && absint( $context['object_id'] ) > 0;

	if ( $requires_object_id ) {
		if ( ! $has_object_id ) {
			return false;
		}

		if ( '' === $object_cap ) {
			return true;
		}

		return current_user_can( $object_cap, absint( $context['object_id'] ) );
	}

	if ( $has_object_id && '' !== $object_cap ) {
		return current_user_can( $object_cap, absint( $context['object_id'] ) );
	}

	return true;
}

/**
 * Ensure directory is protected (htaccess/index.php).
 *
 * @param string $target_dir Absolute path to directory.
 * @return bool
 */
function thespl_ensure_directory_protection( $target_dir ) {
	if ( ! thespl_init_filesystem() ) {
		return false;
	}
	global $wp_filesystem;

	if ( ! $wp_filesystem->is_dir( $target_dir ) ) {
		return false;
	}

	$htaccess  = $target_dir . '/.htaccess';
	$index_php = $target_dir . '/index.php';

	// 1. Create .htaccess if missing.
	if ( ! $wp_filesystem->exists( $htaccess ) ) {
		$content = "# BEGIN The Studio Player Protection\n" .
				"<IfModule mod_rewrite.c>\n" .
				"RewriteEngine On\n" .
				"# Whitelist: Allow only JSON files\n" .
				"RewriteCond %{REQUEST_URI} !\\.json$ [NC]\n" .
				"# Deny everything else\n" .
				"RewriteRule ^.*$ - [F,L]\n" .
				"</IfModule>\n" .
				'# END The Studio Player Protection';
		$wp_filesystem->put_contents( $htaccess, $content, 0644 );
	}

	// 2. Create index.php if missing.
	if ( ! $wp_filesystem->exists( $index_php ) ) {
		$wp_filesystem->put_contents( $index_php, "<?php\n// Silence is golden.\n", 0644 );
	}

	return true;
}

/**
 * Get forced upload subdirectory.
 *
 * @return string Sanitized upload subdirectory.
 */
function thespl_get_forced_upload_subdir() {
	$forced = defined( 'THESPL_FORCED_UPLOAD_SUBDIR' ) ? (string) THESPL_FORCED_UPLOAD_SUBDIR : '';
	$forced = trim( $forced );
	if ( '' === $forced ) {
		return '';
	}
	return thespl_sanitize_subdir_path( $forced );
}

/**
 * Get stable tag version from readme.txt.
 *
 * @return string
 */
function thespl_get_readme_stable_tag_version() {
	global $wp_filesystem;
	$readme = trailingslashit( THESPL_PLUGIN_DIR ) . 'readme.txt';

	if ( ! $wp_filesystem && ! thespl_init_filesystem() ) {
		return defined( 'THESPL_VERSION' ) ? (string) THESPL_VERSION : '';
	}

	if ( $wp_filesystem->exists( $readme ) ) {
		$content = (string) $wp_filesystem->get_contents( $readme );
		if ( preg_match( '/Stable\s+tag:\s*([\S]+)/i', $content, $m ) ) {
			return (string) $m[1];
		}
	}
	return defined( 'THESPL_VERSION' ) ? (string) THESPL_VERSION : '';
}

/**
 * Validate file content with basic FLAC header and JSON structure checks.
 *
 * @param string $path Absolute file path.
 * @return bool
 */
function thespl_validate_file_content( $path ) {
	$ext = strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );

	// 1. FLAC Validation.
	if ( 'flac' === $ext ) {
		global $wp_filesystem;
		if ( ! $wp_filesystem && ! thespl_init_filesystem() ) {
			return false;
		}
		$content = $wp_filesystem->get_contents( $path );
		if ( false === $content || strlen( $content ) < 4 ) {
			return false;
		}
		// Header: fLaC (4).
		$magic = substr( $content, 0, 4 );
		if ( 'fLaC' !== $magic ) {
			return false;
		}

		return true;
	}

	// 2. JSON Validation (Waveform).
	if ( 'json' === $ext ) {
		global $wp_filesystem;
		if ( ! $wp_filesystem && ! thespl_init_filesystem() ) {
			return false;
		}
		$content = $wp_filesystem->get_contents( $path );
		$json    = json_decode( $content );
		return ( null !== $json );
	}

	return true;
}

/**
 * Read a local file segment safely without loading the full file into memory.
 *
 * @param string $file_path Absolute local file path.
 * @param int    $offset    Byte offset from file start.
 * @param int    $length    Number of bytes to read.
 * @return string Segment content or empty string on failure.
 */
function thespl_read_local_file_segment( $file_path, $offset, $length ) {
	$offset = (int) $offset;
	$length = (int) $length;
	if ( ! is_string( $file_path ) || '' === $file_path || $offset < 0 || $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 seek-based partial-read API.
	$handle = fopen( $file_path, 'rb' );
	if ( false === $handle ) {
		return '';
	}

	$segment = '';

	if ( 0 !== $offset ) {
		$seek_result = fseek( $handle, $offset );
		if ( 0 !== $seek_result ) {
			fclose( $handle );
			return '';
		}
	}

	$remaining = $length;
	while ( $remaining > 0 && ! feof( $handle ) ) {
		$chunk = fread( $handle, min( 8192, $remaining ) );
		if ( false === $chunk ) {
			$segment = '';
			break;
		}
		if ( '' === $chunk ) {
			break;
		}
		$segment   .= $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 $segment;
}

/**
 * Generate a unique fingerprint for a file without reading the entire content.
 *
 * Uses a "Smart-Hash" approach: First 8KB + File Size + Last 8KB.
 * This is extremely fast even for multi-gigabyte files and provides a very high
 * probability of uniqueness for audio files.
 *
 * @param string $file_path Absolute path to the file.
 * @return string MD5 hash (fingerprint) or empty string on failure.
 */
function thespl_generate_file_fingerprint( $file_path ) {
	global $wp_filesystem;

	// Ensure filesystem is initialized.
	if ( ! $wp_filesystem ) {
		thespl_init_filesystem();
	}

	if ( ! is_string( $file_path ) || '' === $file_path ) {
		return '';
	}

	// Use WP_Filesystem for existence and readability checks.
	if ( ! $wp_filesystem->exists( $file_path ) || ! $wp_filesystem->is_readable( $file_path ) ) {
		return '';
	}

	$size = $wp_filesystem->size( $file_path );
	if ( false === $size || 0 === (int) $size ) {
		return '';
	}

	$chunk_size = 8192; // 8 KB.
	$header     = '';
	$footer     = '';

	// Read first 8KB and last 8KB to avoid loading large media files into memory.
	$header = thespl_read_local_file_segment( $file_path, 0, $chunk_size );

	if ( $size > ( $chunk_size * 2 ) ) {
		$footer = thespl_read_local_file_segment( $file_path, $size - $chunk_size, $chunk_size );
	} elseif ( $size > $chunk_size ) {
		$remaining = $size - $chunk_size;
		$footer    = thespl_read_local_file_segment( $file_path, $chunk_size, $remaining );
	}

	$data = (string) $size . '|' . (string) $header . '|' . (string) $footer;

	return md5( $data );
}

/**
 * Read normalized audio metadata with on-demand core includes.
 *
 * @param string $file_path Absolute file path.
 * @return array Audio metadata array.
 */
function thespl_read_audio_metadata( $file_path ) {
	$file_path = is_string( $file_path ) ? $file_path : '';
	if ( '' === $file_path ) {
		return array();
	}

	if ( ! function_exists( 'wp_read_audio_metadata' ) ) {
		require_once ABSPATH . 'wp-admin/includes/media.php';
	}

	if ( ! function_exists( 'wp_read_audio_metadata' ) ) {
		return array();
	}

	$metadata = wp_read_audio_metadata( $file_path );

	return is_array( $metadata ) ? $metadata : array();
}

/**
 * Generate attachment metadata with on-demand core includes.
 *
 * @param int    $attachment_id Attachment ID.
 * @param string $file_path     Absolute file path.
 * @return array Attachment metadata array.
 */
function thespl_generate_attachment_metadata( $attachment_id, $file_path ) {
	$attachment_id = (int) $attachment_id;
	$file_path     = is_string( $file_path ) ? $file_path : '';
	$is_audio      = false;
	$audio_meta    = array();

	if ( $attachment_id <= 0 || '' === $file_path ) {
		return array();
	}

	if ( '' !== $file_path ) {
		$filetype = wp_check_filetype( basename( $file_path ), null );
		$mime     = isset( $filetype['type'] ) && is_string( $filetype['type'] ) ? $filetype['type'] : '';
		$is_audio = ( '' !== $mime && 0 === strpos( $mime, 'audio/' ) );
	}

	if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
		require_once ABSPATH . 'wp-admin/includes/image.php';
	}

	if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
		return array();
	}

	if ( $is_audio ) {
		$audio_meta = thespl_read_audio_metadata( $file_path );
	}

	$metadata = wp_generate_attachment_metadata( $attachment_id, $file_path );
	if ( ! is_array( $metadata ) ) {
		return array();
	}

	if ( ! empty( $audio_meta ) ) {
		$audio_merge_keys = array(
			'length',
			'length_formatted',
			'filesize',
			'dataformat',
			'codec',
			'channels',
			'sample_rate',
		);

		foreach ( $audio_merge_keys as $audio_key ) {
			if ( ! isset( $metadata[ $audio_key ] ) && isset( $audio_meta[ $audio_key ] ) ) {
				$metadata[ $audio_key ] = $audio_meta[ $audio_key ];
			}
		}
	}

	return $metadata;
}

/**
 * Handle a media sideload with on-demand core includes.
 *
 * @param array       $file_array Array that represents a $_FILES upload array.
 * @param int         $post_id    Optional. Post ID to associate the media with.
 * @param string|null $desc       Optional. Description of the side-loaded file.
 * @param array       $post_data  Optional. Attachment post data overrides.
 * @return int|WP_Error Attachment ID on success, WP_Error on failure.
 */
function thespl_media_handle_sideload( $file_array, $post_id = 0, $desc = null, $post_data = array() ) {
	if ( ! function_exists( 'download_url' ) ) {
		require_once ABSPATH . 'wp-admin/includes/file.php';
	}

	if ( ! function_exists( 'media_handle_sideload' ) ) {
		require_once ABSPATH . 'wp-admin/includes/media.php';
	}

	if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
		require_once ABSPATH . 'wp-admin/includes/image.php';
	}

	if ( ! function_exists( 'media_handle_sideload' ) ) {
		return new WP_Error( 'thespl_missing_media_sideload', __( 'WordPress media sideload helpers are unavailable.', 'the-studio-player' ) );
	}

	if ( ! function_exists( 'download_url' ) || ! function_exists( 'wp_generate_attachment_metadata' ) ) {
		return new WP_Error( 'thespl_missing_media_dependencies', __( 'Required WordPress media helpers are unavailable.', 'the-studio-player' ) );
	}

	return media_handle_sideload( $file_array, (int) $post_id, $desc, $post_data );
}

/**
 * Register a file in WordPress Media Library.
 *
 * @param string $file_path Absolute file path.
 * @param string $url       Public URL to the file.
 * @return int Attachment ID or 0.
 */
function thespl_register_media_attachment( $file_path, $url ) {
	global $wp_filesystem;
	// Ensure filesystem is init.
	if ( ! $wp_filesystem ) {
		thespl_init_filesystem();
	}

	// Check if file exists.
	if ( ! $wp_filesystem->exists( $file_path ) ) {
		return 0;
	}

	// Check mime type.
	$filetype = wp_check_filetype( basename( $file_path ), null );

	// Fix for JSON if WP doesn't detect it automatically.
	if ( empty( $filetype['type'] ) && 'json' === strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) ) ) {
		$filetype['type'] = 'application/json';
		$filetype['ext']  = 'json';
	}

	// Fix for FLAC if WP doesn't detect it automatically.
	if ( empty( $filetype['type'] ) && 'flac' === strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) ) ) {
		$filetype['type'] = 'audio/flac';
		$filetype['ext']  = 'flac';
	}

	// --- Generate Description Metadata (Best Practice UX). ---
	$description = '';
	$ext         = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );

	if ( 'flac' === $ext ) {
		global $wp_filesystem;
		if ( ! $wp_filesystem && ! thespl_init_filesystem() ) {
			return 0;
		}
		$content = $wp_filesystem->get_contents( $file_path );
		if ( false !== $content && strlen( $content ) >= 22 ) {
			$raw  = substr( $content, 18, 4 );
			$data = unpack( 'N', $raw )[1];
			$sr   = ( $data >> 12 ) & 0xFFFFF;
			$ch   = ( ( $data >> 9 ) & 0x07 ) + 1;
			$bps  = ( ( $data >> 4 ) & 0x1F ) + 1;

			// Format Description.
			$variants = $ch / 2;
			/* translators: %d: Number of variants */
			$variant_label = ( 1 === $variants ) ? __( '1 Variant', 'the-studio-player' ) : sprintf( __( '%d Variants', 'the-studio-player' ), $variants );
			$sr_display    = str_replace( '.0', '', number_format( $sr / 1000, 1 ) ) . ' kHz';

			/* translators: 1: Number of channels, 2: Variant label, 3: Sample rate, 4: Bit depth */
			$description = sprintf(
				__( "Format: FLAC Audio\nChannels: %1\$d (%2\$s)\nSample Rate: %3\$s\nBit Depth: %4\$d-bit\n\nGenerated by The Studio Player Muxer", 'the-studio-player' ),
				$ch,
				$variant_label,
				$sr_display,
				$bps
			);
		}
	} elseif ( 'json' === $ext ) {
		// Try to link to the audio file if possible (heuristic based on filename).
		$base_name = preg_replace( '/\.json$/i', '', basename( $file_path ) );
		$flac_name = $base_name . '.flac';
		$flac_path = dirname( $file_path ) . '/' . $flac_name;

		$linked_info = '';
		if ( $wp_filesystem->exists( $flac_path ) ) {
			$content_flac = $wp_filesystem->get_contents( $flac_path );
			if ( false !== $content_flac && strlen( $content_flac ) >= 22 ) {
				$raw     = substr( $content_flac, 18, 4 );
				$data    = unpack( 'N', $raw )[1];
				$sr      = ( $data >> 12 ) & 0xFFFFF;
				$ch      = ( ( $data >> 9 ) & 0x07 ) + 1;
				$sr_disp = str_replace( '.0', '', number_format( $sr / 1000, 1 ) );
				/* translators: 1: Linked audio filename, 2: Number of channels, 3: Sample rate */
				$linked_info = sprintf( __( "\nLinked Audio: %1\$s\nAudio Specs: %2\$d Channels, %3\$s kHz", 'the-studio-player' ), $flac_name, $ch, $sr_disp );
			}
		}

		/* translators: %s: Linked audio info */
		$description = sprintf( __( "Waveform Visualization Data%s\nGenerated by The Studio Player", 'the-studio-player' ), $linked_info );
	}

	// Prepare attachment data.
	$attachment = array(
		'guid'           => $url,
		'post_mime_type' => $filetype['type'],
		'post_title'     => preg_replace( '/\.[^.]+$/', '', basename( $file_path ) ),
		'post_content'   => $description,
		'post_status'    => 'inherit',
	);

	// Check if attachment already exists (avoid duplicates).
	// We check by _wp_attached_file post meta via a uploads-relative path, using the same
	// normalization logic as thespl_get_upload_relative_path() to avoid platform issues.
	$relative_path = thespl_get_upload_relative_path( $file_path );
	if ( '' === $relative_path ) {
		$relative_path = ltrim( str_replace( '\\', '/', $file_path ), '/' );
	}

	$cache_key   = 'thespl_attachment_' . md5( $relative_path );
	$cache_group = 'the_studio_player';
	$attach_id   = wp_cache_get( $cache_key, $cache_group );

	if ( false === $attach_id ) {
		// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Admin-only meta lookup via WP APIs; minor performance trade-off accepted for safety.
		$existing = get_posts(
			array(
				'post_type'      => 'attachment',
				'post_status'    => 'inherit',
				'meta_key'       => '_wp_attached_file',
				'meta_value'     => $relative_path,
				'posts_per_page' => 1,
				'fields'         => 'ids',
				'no_found_rows'  => true,
				'cache_results'  => false,
			)
		);
		// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Re-enable slow meta-query sniff for other code paths.
		if ( ! empty( $existing ) ) {
			$attach_id = (int) $existing[0];
			wp_cache_set( $cache_key, $attach_id, $cache_group );
		} else {
			$attach_id = 0;
		}
	}

	if ( $attach_id ) {
		// Update existing attachment.
		$attachment['ID'] = $attach_id;
		wp_update_post( $attachment );
	} else {
		// Insert new attachment.
		$attach_id = wp_insert_attachment( $attachment, $file_path );
		if ( is_wp_error( $attach_id ) ) {
			return 0;
		}
	}

	// Generate metadata (required for audio length etc.).
	$attach_data = thespl_generate_attachment_metadata( $attach_id, $file_path );
	if ( empty( $attach_data ) ) {
		return 0;
	}

	wp_update_attachment_metadata( $attach_id, $attach_data );

	// --- Generate & Store Fingerprint (Silent Background Task) ---
	// This fingerprint is used for data recovery and consistency checks.
	$fingerprint = thespl_generate_file_fingerprint( $file_path );
	if ( '' !== $fingerprint ) {
		update_post_meta( $attach_id, '_thespl_fingerprint', $fingerprint );
	}

	return $attach_id;
}


/**
 * Get the current opt-in state for remote content.
 *
 * Returns 'yes', 'no' or an empty string if not set or invalid.
 *
 * @return string Current opt-in state.
 */
function thespl_get_remote_opt_in_state() {
	$state = get_option( 'thespl_remote_content_opt_in', '' );
	if ( ! is_string( $state ) ) {
		return '';
	}
	$state = strtolower( trim( $state ) );
	if ( 'yes' === $state || 'no' === $state ) {
		return $state;
	}
	return '';
}

/**
 * Check whether remote content opt-in is currently enabled.
 *
 * @return bool True when remote content may be loaded.
 */
function thespl_remote_opt_in_enabled() {
	return 'yes' === thespl_get_remote_opt_in_state();
}

/**
 * Get the base URL for the THESPL Remote REST API.
 *
 * Value can be filtered via the `thespl_remote_base_api_url` filter.
 *
 * @return string Base API URL without trailing slash.
 */
function thespl_get_remote_base_api_url() {
	$base = apply_filters( 'thespl_remote_base_api_url', 'https://studioplayer.de/wp-json/thespl-remote/v1' );
	if ( ! is_string( $base ) ) {
		$base = '';
	}
	$base = trim( $base );
	if ( '' === $base ) {
		return '';
	}
	return untrailingslashit( $base );
}

/**
 * Build common query arguments for THESPL Remote requests.
 *
 * @return array Associative array with version, plan and locale.
 */
function thespl_get_remote_common_query_args() {
	if ( ! thespl_remote_opt_in_enabled() ) {
		return array();
	}
	$version = defined( 'THESPL_VERSION' ) ? (string) THESPL_VERSION : '';
	$plan    = thespl_is_pro_active() ? 'pro' : 'free';
	$locale  = get_locale();

	return array(
		'plugin_version' => $version,
		'plan'           => $plan,
		'locale'         => $locale,
	);
}

/**
 * Get dismissed remote banners for the current user.
 *
 * @return array Map of banner IDs to dismissal metadata.
 */
function thespl_get_remote_dismissed_banners() {
	$user_id = get_current_user_id();
	if ( ! $user_id ) {
		return array();
	}
	$data = get_user_meta( $user_id, 'thespl_remote_dismissed_banners', true );
	if ( ! is_array( $data ) ) {
		$data = array();
	}
	return $data;
}

/**
 * Check whether a remote banner has been dismissed for the current user.
 *
 * @param int $banner_id Banner ID from remote API.
 * @return bool True if dismissed and still within dismissal period.
 */
function thespl_is_remote_banner_dismissed( $banner_id ) {
	$banner_id = (int) $banner_id;
	if ( $banner_id <= 0 ) {
		return false;
	}
	$user_id = get_current_user_id();
	if ( ! $user_id ) {
		return false;
	}
	$data = thespl_get_remote_dismissed_banners();
	if ( ! isset( $data[ $banner_id ] ) || ! is_array( $data[ $banner_id ] ) ) {
		return false;
	}
	$entry = $data[ $banner_id ];
	$until = isset( $entry['until'] ) ? (int) $entry['until'] : 0;
	$now   = time();
	if ( 0 === $until ) {
		return true;
	}
	if ( $now < $until ) {
		return true;
	}
	unset( $data[ $banner_id ] );
	update_user_meta( $user_id, 'thespl_remote_dismissed_banners', $data );
	return false;
}

/**
 * AJAX handler to mark a remote banner as dismissed for the current user.
 *
 * @return void
 */
function thespl_remote_dismiss_banner() {
	check_ajax_referer( 'thespl_remote_dismiss_banner' );
	if ( ! thespl_current_user_can_policy( 'plugin_access' ) ) {
		wp_send_json_error( array( 'message' => 'forbidden' ), 403 );
	}
	$banner_id = isset( $_POST['banner_id'] ) ? absint( wp_unslash( $_POST['banner_id'] ) ) : 0;
	if ( $banner_id <= 0 ) {
		wp_send_json_error( array( 'message' => 'invalid_id' ), 400 );
	}
	$days = isset( $_POST['days'] ) ? absint( wp_unslash( $_POST['days'] ) ) : 0;
	if ( $days < 0 ) {
		$days = 0;
	}
	$user_id = get_current_user_id();
	if ( ! $user_id ) {
		wp_send_json_error( array( 'message' => 'no_user' ), 400 );
	}
	$data  = thespl_get_remote_dismissed_banners();
	$until = 0;
	if ( $days > 0 ) {
		$until = time() + ( $days * DAY_IN_SECONDS );
	}
	$data[ $banner_id ] = array(
		'until' => $until,
	);
	update_user_meta( $user_id, 'thespl_remote_dismissed_banners', $data );
	wp_send_json_success();
}
add_action( 'wp_ajax_thespl_remote_dismiss_banner', 'thespl_remote_dismiss_banner' );

/**
 * Print footer script for remote banner dismissal handling.
 * Fetch remote banners.
 *
 * @param string $placement Placement slug (kept for backward compatibility).
 * @return array List of banner items.
 */
function thespl_get_remote_banners( $placement = '' ) {
	if ( ! thespl_remote_opt_in_enabled() ) {
		return array();
	}
	unset( $placement );
	static $cached_items = null;
	if ( null !== $cached_items ) {
		return $cached_items;
	}
	$base = thespl_get_remote_base_api_url();
	if ( '' === $base ) {
		$cached_items = array();
		return $cached_items;
	}
	$args     = thespl_get_remote_common_query_args();
	$url      = $base . '/banners';
	$url      = add_query_arg( $args, $url );
	$response = wp_remote_get(
		$url,
		array(
			'timeout'     => 5,
			'redirection' => 3,
		)
	);
	if ( is_wp_error( $response ) ) {
		$cached_items = array();
		return $cached_items;
	}
	$code = (int) wp_remote_retrieve_response_code( $response );
	if ( 200 !== $code ) {
		$cached_items = array();
		return $cached_items;
	}
	$body = wp_remote_retrieve_body( $response );
	if ( ! is_string( $body ) || '' === $body ) {
		$cached_items = array();
		return $cached_items;
	}
	$data = json_decode( $body, true );
	if ( ! is_array( $data ) || ! isset( $data['banners'] ) || ! is_array( $data['banners'] ) ) {
		$cached_items = array();
		return $cached_items;
	}
	$items = array();
	foreach ( $data['banners'] as $item ) {
		if ( is_array( $item ) ) {
			$items[] = $item;
		}
	}
	$cached_items = $items;
	return $cached_items;
}

/**
 * Render remote admin banners on Studio Player admin screens.
 *
 * @return void
 */
function thespl_render_remote_admin_banners() {
	$remote_banners = thespl_get_remote_banners();
	if ( empty( $remote_banners ) ) {
		return;
	}
	$warning_banners = array();
	$other_banners   = array();
	foreach ( $remote_banners as $banner ) {
		if ( ! is_array( $banner ) ) {
			$other_banners[] = $banner;
			continue;
		}
		$type = 'info';
		if ( isset( $banner['type'] ) && is_string( $banner['type'] ) ) {
			$type = sanitize_key( $banner['type'] );
		}
		if ( 'warning' === $type ) {
			$warning_banners[] = $banner;
		} else {
			$other_banners[] = $banner;
		}
	}
	$remote_banners = array_merge( $warning_banners, $other_banners );
	?>
	<div class="thespl-admin-remote-banners">
		<?php foreach ( $remote_banners as $banner ) : ?>
			<?php
			if ( ! is_array( $banner ) ) {
				continue;
			}
			$body = '';
			if ( isset( $banner['body'] ) && is_string( $banner['body'] ) ) {
				$body = $banner['body'];
			}
			$type = 'info';
			if ( isset( $banner['type'] ) && is_string( $banner['type'] ) ) {
				$type = sanitize_key( $banner['type'] );
			}
			$banner_id    = 0;
			$dismissible  = false;
			$dismiss_days = 0;
			if ( isset( $banner['id'] ) ) {
				$banner_id = (int) $banner['id'];
			}
			if ( isset( $banner['dismissible'] ) ) {
				$dismissible = (bool) $banner['dismissible'];
			}
			if ( isset( $banner['dismissDays'] ) && is_numeric( $banner['dismissDays'] ) ) {
				$dismiss_days = (int) $banner['dismissDays'];
			}
			if ( $dismissible && $banner_id > 0 && thespl_is_remote_banner_dismissed( $banner_id ) ) {
				continue;
			}
			$notice_class = 'notice-info';
			if ( 'success' === $type ) {
				$notice_class = 'notice-success';
			} elseif ( 'warning' === $type ) {
				$notice_class = 'notice-warning';
			} elseif ( 'error' === $type ) {
				$notice_class = 'notice-error';
			}
			$cta        = array();
			$cta_label  = '';
			$cta_url    = '';
			$cta_target = '_self';
			$cta_type   = 'button';
			if ( isset( $banner['cta'] ) && is_array( $banner['cta'] ) ) {
				$cta = $banner['cta'];
			}
			if ( isset( $cta['label'] ) && is_string( $cta['label'] ) ) {
				$cta_label = $cta['label'];
			}
			if ( isset( $cta['url'] ) && is_string( $cta['url'] ) ) {
				$cta_url = $cta['url'];
			}
			if ( isset( $cta['target'] ) && is_string( $cta['target'] ) ) {
				$cta_target = $cta['target'];
			}
			if ( isset( $cta['type'] ) && is_string( $cta['type'] ) ) {
				$cta_type = sanitize_key( $cta['type'] );
			}
			$banner_attrs = '';
			if ( $dismissible && $banner_id > 0 ) {
				$banner_attrs = ' data-thespl-remote-banner-id="' . esc_attr( (string) $banner_id ) . '" data-thespl-dismiss-days="' . esc_attr( (string) $dismiss_days ) . '"';
			}
			?>
			<div class="notice <?php echo esc_attr( $notice_class ); ?> thespl-remote-banner<?php echo $dismissible ? ' is-dismissible' : ''; ?>"<?php echo wp_kses_post( $banner_attrs ); ?>>
				<?php if ( '' !== $body ) : ?>
					<p><?php echo wp_kses_post( $body ); ?></p>
				<?php endif; ?>
				<?php if ( '' !== $cta_label && '' !== $cta_url ) : ?>
					<?php
					$cta_target = ( '_blank' === $cta_target ) ? '_blank' : '_self';
					$rel_attr   = ( '_blank' === $cta_target ) ? 'noopener noreferrer' : '';
					?>
					<?php if ( 'text' === $cta_type ) : ?>
						<p><a href="<?php echo esc_url( $cta_url ); ?>" target="<?php echo esc_attr( $cta_target ); ?>"<?php echo ( '' !== $rel_attr ) ? ' rel="' . esc_attr( $rel_attr ) . '"' : ''; ?>><?php echo esc_html( $cta_label ); ?></a></p>
					<?php else : ?>
						<p><a href="<?php echo esc_url( $cta_url ); ?>" target="<?php echo esc_attr( $cta_target ); ?>"<?php echo ( '' !== $rel_attr ) ? ' rel="' . esc_attr( $rel_attr ) . '"' : ''; ?> class="button button-primary"><?php echo esc_html( $cta_label ); ?></a></p>
					<?php endif; ?>
				<?php endif; ?>
			</div>
		<?php endforeach; ?>
	</div>
	<?php
}

/**
 * Render global remote warning banners on WordPress admin screens.
 *
 * @return void
 */
function thespl_render_global_remote_warning_banners() {
	if ( ! is_admin() ) {
		return;
	}
	if ( ! current_user_can( 'manage_options' ) ) {
		return;
	}
	$settings = thespl_get_player_settings();
	if ( empty( $settings['remote_global_warning_banners_enabled'] ) ) {
		return;
	}
	if ( function_exists( 'thespl_is_plugin_admin_screen' ) && thespl_is_plugin_admin_screen() ) {
		return;
	}
	$remote_banners = thespl_get_remote_banners();
	if ( empty( $remote_banners ) ) {
		return;
	}
	foreach ( $remote_banners as $banner ) {
		if ( ! is_array( $banner ) ) {
			continue;
		}
		$type = 'info';
		if ( isset( $banner['type'] ) && is_string( $banner['type'] ) ) {
			$type = sanitize_key( $banner['type'] );
		}
		if ( 'warning' !== $type ) {
			continue;
		}
		$placements = array();
		if ( isset( $banner['placements'] ) && is_array( $banner['placements'] ) ) {
			$placements = $banner['placements'];
		}
		if ( empty( $placements ) || ! in_array( 'global', $placements, true ) ) {
			continue;
		}
		$body = '';
		if ( isset( $banner['body'] ) && is_string( $banner['body'] ) ) {
			$body = $banner['body'];
		}
		$banner_id    = 0;
		$dismissible  = false;
		$dismiss_days = 0;
		if ( isset( $banner['id'] ) ) {
			$banner_id = (int) $banner['id'];
		}
		if ( isset( $banner['dismissible'] ) ) {
			$dismissible = (bool) $banner['dismissible'];
		}
		if ( isset( $banner['dismissDays'] ) && is_numeric( $banner['dismissDays'] ) ) {
			$dismiss_days = (int) $banner['dismissDays'];
		}
		if ( $dismissible && $banner_id > 0 && thespl_is_remote_banner_dismissed( $banner_id ) ) {
			continue;
		}
		$notice_class = 'notice-warning';
		$cta          = array();
		$cta_label    = '';
		$cta_url      = '';
		$cta_target   = '_self';
		$cta_type     = 'button';
		if ( isset( $banner['cta'] ) && is_array( $banner['cta'] ) ) {
			$cta = $banner['cta'];
		}
		if ( isset( $cta['label'] ) && is_string( $cta['label'] ) ) {
			$cta_label = $cta['label'];
		}
		if ( isset( $cta['url'] ) && is_string( $cta['url'] ) ) {
			$cta_url = $cta['url'];
		}
		if ( isset( $cta['target'] ) && is_string( $cta['target'] ) ) {
			$cta_target = $cta['target'];
		}
		if ( isset( $cta['type'] ) && is_string( $cta['type'] ) ) {
			$cta_type = sanitize_key( $cta['type'] );
		}
		$banner_attrs = '';
		if ( $dismissible && $banner_id > 0 ) {
			$banner_attrs = ' data-thespl-remote-banner-id="' . esc_attr( (string) $banner_id ) . '" data-thespl-dismiss-days="' . esc_attr( (string) $dismiss_days ) . '"';
		}
		?>
		<div class="notice <?php echo esc_attr( $notice_class ); ?> thespl-remote-banner<?php echo $dismissible ? ' is-dismissible' : ''; ?>"<?php echo wp_kses_post( $banner_attrs ); ?>>
			<?php if ( '' !== $body ) : ?>
				<p><?php echo wp_kses_post( $body ); ?></p>
			<?php endif; ?>
			<?php if ( '' !== $cta_label && '' !== $cta_url ) : ?>
				<?php
				$cta_target = ( '_blank' === $cta_target ) ? '_blank' : '_self';
				$rel_attr   = ( '_blank' === $cta_target ) ? 'noopener noreferrer' : '';
				?>
				<?php if ( 'text' === $cta_type ) : ?>
					<p><a href="<?php echo esc_url( $cta_url ); ?>" target="<?php echo esc_attr( $cta_target ); ?>"<?php echo ( '' !== $rel_attr ) ? ' rel="' . esc_attr( $rel_attr ) . '"' : ''; ?>><?php echo esc_html( $cta_label ); ?></a></p>
				<?php else : ?>
					<p><a href="<?php echo esc_url( $cta_url ); ?>" target="<?php echo esc_attr( $cta_target ); ?>"<?php echo ( '' !== $rel_attr ) ? ' rel="' . esc_attr( $rel_attr ) . '"' : ''; ?> class="button button-primary"><?php echo esc_html( $cta_label ); ?></a></p>
				<?php endif; ?>
			<?php endif; ?>
		</div>
		<?php
	}
}

add_action( 'admin_notices', 'thespl_render_global_remote_warning_banners' );

/**
 * Remove style/script blocks from remote content before kses sanitization.
 *
 * This prevents stripped tags from leaving their inner text behind in the admin UI.
 *
 * @param string $html Remote HTML snippet.
 * @return string Cleaned HTML snippet.
 */
function thespl_strip_remote_content_blocks( $html ) {
	if ( ! is_string( $html ) || '' === $html ) {
		return '';
	}

	$html = preg_replace( '#<style\b[^>]*>.*?</style>#is', '', $html );
	$html = preg_replace( '#<script\b[^>]*>.*?</script>#is', '', $html );

	$manual_wrapper_pos = stripos( $html, 'class="tsp-manual"' );
	if ( false === $manual_wrapper_pos ) {
		$manual_wrapper_pos = stripos( $html, "class='tsp-manual'" );
	}

	if ( false !== $manual_wrapper_pos && $manual_wrapper_pos > 0 ) {
		$prefix = substr( $html, 0, $manual_wrapper_pos );
		if ( is_string( $prefix ) && '' !== $prefix && false !== strpos( $prefix, '.tsp-manual' ) ) {
			$html = substr( $html, $manual_wrapper_pos );
			$html = '<div ' . $html;
		}
	}

	return is_string( $html ) ? $html : '';
}

/**
 * Fetch remote documentation entries for a given context.
 *
 * @param string $context Context slug (e.g. "dashboard").
 * @param string $kind    Documentation type filter (e.g. "quickstart" or "full").
 * @return array List of documentation items.
 */
function thespl_get_remote_docs( $context, $kind = '' ) {
	if ( ! thespl_remote_opt_in_enabled() ) {
		return array();
	}
	$base = thespl_get_remote_base_api_url();
	if ( '' === $base ) {
		return array();
	}
	$context         = is_string( $context ) ? sanitize_key( $context ) : '';
	$args            = thespl_get_remote_common_query_args();
	$args['context'] = $context;
	if ( is_string( $kind ) && '' !== $kind ) {
		$kind         = sanitize_key( $kind );
		$args['kind'] = $kind;
	}
	$url      = $base . '/docs';
	$url      = add_query_arg( $args, $url );
	$response = wp_remote_get(
		$url,
		array(
			'timeout'     => 5,
			'redirection' => 3,
		)
	);
	if ( is_wp_error( $response ) ) {
		return array();
	}
	$code = (int) wp_remote_retrieve_response_code( $response );
	if ( 200 !== $code ) {
		return array();
	}
	$body = wp_remote_retrieve_body( $response );
	if ( ! is_string( $body ) || '' === $body ) {
		return array();
	}
	$data = json_decode( $body, true );
	if ( ! is_array( $data ) || ! isset( $data['docs'] ) || ! is_array( $data['docs'] ) ) {
		return array();
	}
	$items = array();
	foreach ( $data['docs'] as $item ) {
		if ( is_array( $item ) ) {
			$items[] = $item;
		}
	}
	return $items;
}

/**
 * Fetch remote onboarding entries for a given context.
 *
 * @param string $context Context slug (e.g. "dashboard").
 * @return array List of onboarding items.
 */
function thespl_get_remote_onboarding( $context ) {
	if ( ! thespl_remote_opt_in_enabled() ) {
		return array();
	}
	$base = thespl_get_remote_base_api_url();
	if ( '' === $base ) {
		return array();
	}
	$context         = is_string( $context ) ? sanitize_key( $context ) : '';
	$args            = thespl_get_remote_common_query_args();
	$args['context'] = $context;
	$url             = $base . '/onboarding';
	$url             = add_query_arg( $args, $url );
	$response        = wp_remote_get(
		$url,
		array(
			'timeout'     => 5,
			'redirection' => 3,
		)
	);
	if ( is_wp_error( $response ) ) {
		return array();
	}
	$code = (int) wp_remote_retrieve_response_code( $response );
	if ( 200 !== $code ) {
		return array();
	}
	$body = wp_remote_retrieve_body( $response );
	if ( ! is_string( $body ) || '' === $body ) {
		return array();
	}
	$data = json_decode( $body, true );
	if ( ! is_array( $data ) || ! isset( $data['onboarding'] ) || ! is_array( $data['onboarding'] ) ) {
		return array();
	}
	$items = array();
	foreach ( $data['onboarding'] as $item ) {
		if ( is_array( $item ) ) {
			$items[] = $item;
		}
	}
	return $items;
}

/**
 * Normalize onboarding video URLs to embeddable YouTube URLs when possible.
 *
 * @param string $url Raw video URL.
 * @return string Normalized embed URL or original URL.
 */
function thespl_normalize_onboarding_video_url( $url ) {
	if ( ! is_string( $url ) ) {
		return '';
	}
	$url = trim( $url );
	if ( '' === $url ) {
		return '';
	}
	$parsed = wp_parse_url( $url );
	if ( ! is_array( $parsed ) || ! isset( $parsed['host'] ) ) {
		return $url;
	}
	$host = strtolower( $parsed['host'] );
	$id   = '';
	if ( 'youtu.be' === $host ) {
		$path = isset( $parsed['path'] ) ? trim( $parsed['path'], '/' ) : '';
		if ( '' !== $path ) {
			$id = $path;
		}
	} elseif ( false !== strpos( $host, 'youtube.' ) ) {
		$path = isset( $parsed['path'] ) ? $parsed['path'] : '';
		if ( is_string( $path ) && 0 === strpos( $path, '/embed/' ) ) {
			return $url;
		}
		if ( isset( $parsed['query'] ) && is_string( $parsed['query'] ) ) {
			parse_str( $parsed['query'], $qs );
			if ( isset( $qs['v'] ) && is_string( $qs['v'] ) && '' !== $qs['v'] ) {
				$id = $qs['v'];
			}
		}
	}
	if ( '' === $id ) {
		return $url;
	}
	return 'https://www.youtube.com/embed/' . rawurlencode( $id ) . '?rel=0';
}

/**
 * Fetch remote changelog entries.
 *
 * @param int $limit Maximum number of entries.
 * @return array List of changelog items.
 */
function thespl_get_remote_changelog( $limit = 5 ) {
	if ( ! thespl_remote_opt_in_enabled() ) {
		return array();
	}
	$base = thespl_get_remote_base_api_url();
	if ( '' === $base ) {
		return array();
	}
	$limit = (int) $limit;
	if ( $limit <= 0 ) {
		$limit = 5;
	}
	$args          = thespl_get_remote_common_query_args();
	$args['limit'] = $limit;
	$url           = $base . '/changelog';
	$url           = add_query_arg( $args, $url );
	$response      = wp_remote_get(
		$url,
		array(
			'timeout'     => 5,
			'redirection' => 3,
		)
	);
	if ( is_wp_error( $response ) ) {
		return array();
	}
	$code = (int) wp_remote_retrieve_response_code( $response );
	if ( 200 !== $code ) {
		return array();
	}
	$body = wp_remote_retrieve_body( $response );
	if ( ! is_string( $body ) || '' === $body ) {
		return array();
	}
	$data = json_decode( $body, true );
	if ( ! is_array( $data ) || ! isset( $data['entries'] ) || ! is_array( $data['entries'] ) ) {
		return array();
	}
	$items = array();
	foreach ( $data['entries'] as $item ) {
		if ( is_array( $item ) ) {
			$items[] = $item;
		}
	}
	return $items;
}
