<?php
/**
 * Custom functions.
 *
 * @package GoDAM
 */

defined( 'ABSPATH' ) || exit;

/**
 * This method is an improved version of PHP's filter_input() and
 * works well on PHP CLI as well which PHP default method does not.
 *
 * Reference:
 * - https://bugs.php.net/bug.php?id=49184
 * - https://bugs.php.net/bug.php?id=54672
 *
 * @param int    $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV.
 * @param string $variable_name Name of a variable to get.
 * @param int    $filter The ID of the filter to apply.
 * @param mixed  $options filter to apply.
 *
 * @return mixed Value of the requested variable on success, FALSE if the filter fails, or NULL if the
 *               variable_name variable is not set.
 */
function rtgodam_filter_input( $type, $variable_name, $filter = FILTER_DEFAULT, $options = 0 ) {

	if ( php_sapi_name() !== 'cli' ) {

		/**
		 * We can not have code coverage since.
		 * Since this will only execute when sapi is "fpm-fcgi".
		 * While Unit test case run on "cli"
		 */
		// @codeCoverageIgnoreStart

		$sanitized_variable = filter_input( $type, $variable_name, $filter, $options );

		/**
		 * Code is not running on PHP Cli and we are in clear.
		 * Use the PHP method and bail out.
		 */
		if ( ! empty( $sanitized_variable ) && FILTER_SANITIZE_FULL_SPECIAL_CHARS === $filter ) {
			$sanitized_variable = sanitize_text_field( $sanitized_variable );
		}

		return $sanitized_variable;
		// @codeCoverageIgnoreEnd
	}

	/**
	 * Code is running on PHP Cli and INPUT_SERVER returns NULL
	 * even for set vars when run on Cli
	 * See: https://bugs.php.net/bug.php?id=49184
	 *
	 * This is a workaround for that bug till its resolved in PHP binary
	 * which doesn't look to be anytime soon. This is a friggin' 10 year old bug.
	 */

	$input = '';

	$allowed_html_tags = wp_kses_allowed_html( 'post' );

	/**
	 * Marking the switch() block below to be ignored by PHPCS
	 * because PHPCS squawks on using superglobals like $_POST or $_GET
	 * directly but it can't be helped in this case as this code
	 * is running on Cli.
	 */

	// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE,  WordPress.Security.ValidatedSanitizedInput.MissingUnslash

	switch ( $type ) {

		case INPUT_GET:
			if ( ! isset( $_GET[ $variable_name ] ) ) {
				return null;
			}

			$input = wp_kses( $_GET[ $variable_name ], $allowed_html_tags );
			break;

		case INPUT_POST:
			if ( ! isset( $_POST[ $variable_name ] ) ) {
				return null;
			}

			$input = wp_kses( $_POST[ $variable_name ], $allowed_html_tags );
			break;

		case INPUT_COOKIE:
			if ( ! isset( $_COOKIE[ $variable_name ] ) ) {
				return null;
			}

			$input = wp_kses( $_COOKIE[ $variable_name ], $allowed_html_tags );
			break;

		case INPUT_SERVER:
			if ( ! isset( $_SERVER[ $variable_name ] ) ) {
				return null;
			}

			$input = wp_kses( $_SERVER[ $variable_name ], $allowed_html_tags );
			break;

		case INPUT_ENV:
			if ( ! isset( $_ENV[ $variable_name ] ) ) {
				return null;
			}

			$input = wp_kses( $_ENV[ $variable_name ], $allowed_html_tags );
			break;

		default:
			return null;

	}

	// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE

	return filter_var( $input, $filter );
}

/**
 * Fetch the URL of a media file by its ID.
 *
 * This function retrieves the URL of a media attachment in WordPress based on the provided media ID.
 * It validates the ID, ensures the media exists, and is of the correct type (attachment).
 *
 * @param int $media_id The ID of the media attachment.
 *
 * @return string The URL of the media file, or an empty string if invalid or not found.
 */
function rtgodam_fetch_overlay_media_url( $media_id ) {
	if ( empty( $media_id ) || 0 === intval( $media_id ) ) {
		return '';
	}

	$media = get_post( $media_id );

	if ( ! $media || 'attachment' !== $media->post_type ) {
		return '';
	}

	$media_url = wp_get_attachment_url( $media_id );

	return $media_url ? $media_url : '';
}

/**
 * Generate the HTML for an image-based call-to-action (CTA) overlay.
 *
 * This function creates a dynamic HTML structure for displaying an image CTA overlay.
 * It uses the provided `$layer` data to populate the content, including image, text, and links.
 *
 * @param array $layer Associative array containing CTA details:
 *     - 'image' (int): Media ID for the image.
 *     - 'imageUrlExt' (string): External URL for the image (GoDAM hosted).
 *     - 'cardLayout' (string): Layout type for the CTA card.
 *     - 'imageOpacity' (float): Opacity of the image (default is 1).
 *     - 'imageText' (string): Heading text for the CTA.
 *     - 'imageDescription' (string): Description text for the CTA.
 *     - 'imageLink' (string): URL for the CTA link.
 *     - 'imageCtaButtonText' (string): Text for the CTA button.
 *     - 'imageCtaButtonColor' (string): Background color for the CTA button.
 *
 * @return string The generated HTML string for the image CTA overlay.
 */
function rtgodam_image_cta_html( $layer ) {
	// Determine if the image is a GoDAM hosted media.
	$is_godam_media = isset( $layer['image'] ) && is_string( $layer['image'] ) && str_starts_with( $layer['image'], 'godam_' );

	if ( $is_godam_media && ! empty( $layer['imageUrlExt'] ) ) {
		$image_url = $layer['imageUrlExt'];
	} else {
		$image_url = rtgodam_fetch_overlay_media_url( isset( $layer['image'] ) ? $layer['image'] : 0 );
	}

	// Define allowed layouts for validation.
	$allowed_layouts = array(
		'card-layout--text-imagecover',
		'card-layout--text-image',
		'card-layout--image-bottom',
		'card-layout--image-background',
		'card-layout--imagecover-text',
		'card-layout--image-text',
		'card-layout--image-top',
		'desktop-text-only',
	);

	// Backward compatibility: determine default layout based on imageCtaOrientation.
	$default_layout = 'card-layout--image-text';
	if ( isset( $layer['imageCtaOrientation'] ) && 'landscape' !== $layer['imageCtaOrientation'] ) {
		$default_layout = 'card-layout--image-top';
	}

	$layout = isset( $layer['cardLayout'] ) && in_array( $layer['cardLayout'], $allowed_layouts, true )
		? $layer['cardLayout']
		: $default_layout;

	$has_image            = ! empty( $image_url );
	$image_opacity        = isset( $layer['imageOpacity'] ) ? floatval( $layer['imageOpacity'] ) : 1;
	$image_width          = isset( $layer['imageWidth'] ) ? absint( $layer['imageWidth'] ) : 50;
	$image_text           = isset( $layer['imageText'] ) ? sanitize_text_field( $layer['imageText'] ) : '';
	$image_description    = isset( $layer['imageDescription'] ) ? sanitize_text_field( $layer['imageDescription'] ) : '';
	$image_link           = isset( $layer['imageLink'] ) ? $layer['imageLink'] : '#';
	$cta_background_color = isset( $layer['imageCtaButtonColor'] ) ? sanitize_hex_color( $layer['imageCtaButtonColor'] ) : '#EEAB95';
	$cta_button_text      = ! empty( $layer['imageCtaButtonText'] ) ? sanitize_text_field( $layer['imageCtaButtonText'] ) : __( 'Check now', 'godam' );

	// Ensure opacity is within valid range.
	$image_opacity = max( 0, min( 1, $image_opacity ) );

	// Ensure width is within valid range.
	$image_width = max( 0, min( 100, $image_width ) );

	// If no image is provided, force text-only layout on frontend.
	if ( ! $has_image ) {
		$layout = 'desktop-text-only';
	}

	// Generate image element.
	if ( $has_image ) {
		$image_element = sprintf(
			'<img src="%s" alt="%s" style="opacity: %s;" />',
			esc_url( $image_url ),
			esc_attr__( 'CTA Card Image', 'godam' ),
			esc_attr( $image_opacity )
		);
	} else {
		$image_element = sprintf(
			'<div class="godam-cta-card-image-placeholder" style="opacity: %s;">%s</div>',
			esc_attr( $image_opacity ),
			esc_html__( 'No Image', 'godam' )
		);
	}

	// Generate content element.
	$content_element = '<div class="godam-cta-card-content">';

	if ( ! empty( $image_text ) ) {
		$content_element .= sprintf( '<h2 class="card-title">%s</h2>', esc_html( $image_text ) );
	}

	if ( ! empty( $image_description ) ) {
		$content_element .= sprintf( '<p class="card-description">%s</p>', esc_html( $image_description ) );
	}

	if ( ! empty( $cta_button_text ) || ! empty( $image_link ) ) {
		$content_element .= sprintf(
			'<div class="btns"><a class="godam-cta-btn" href="%s" target="_blank" rel="noopener noreferrer" style="background-color: %s; text-decoration: none;">%s</a></div>',
			esc_url( $image_link ),
			esc_attr( $cta_background_color ),
			esc_html( $cta_button_text )
		);
	}

	$content_element .= '</div>';

	// Handle different layouts.
	$card_content = '';

	if ( 'desktop-text-only' === $layout ) {
		// Text only - no image.
		$card_content = $content_element;
	} elseif ( 'card-layout--image-background' === $layout ) {
		// Image background layout.
		$card_content = sprintf(
			'<div class="godam-cta-card-image-bg" style="background-image: url(\'%s\'); opacity: %s;"></div>%s',
			esc_url( $image_url ),
			esc_attr( $image_opacity ),
			$content_element
		);
	} else {
		// All other layouts with image element.
		$image_content = sprintf( '<div class="godam-cta-card-image">%s</div>', $image_element );

		// Return based on layout order.
		if ( in_array( $layout, array( 'card-layout--text-imagecover', 'card-layout--text-image', 'card-layout--image-bottom' ), true ) ) {
			$card_content = $content_element . $image_content;
		} else {
			$card_content = $image_content . $content_element;
		}
	}

	return sprintf(
		'<div class="godam-cta-overlay-container"><div class="godam-cta-card %s" style="--image-width: %s%%;">%s</div></div>',
		esc_attr( $layout ),
		esc_attr( $image_width ),
		$card_content
	);
}

/**
 * Verify the api key for the plugin and return user data.
 *
 * @param bool $use_for_localize_array Whether to use the data for localizing scripts. Defaults to false.
 * @param int  $timeout                The time in seconds after which the user data should be refreshed.
 * @param bool $force_refresh          Whether to force refresh the API key verification bypassing grace period checks.
 */
function rtgodam_get_user_data( $use_for_localize_array = false, $timeout = HOUR_IN_SECONDS, $force_refresh = false ) {
	$rtgodam_user_data = get_option( 'rtgodam_user_data', false );
	$api_key           = get_option( 'rtgodam-api-key', '' );
	$api_key_status    = rtgodam_get_api_key_status();

	// If no API key is stored, skip all remote verification — there is nothing
	// to verify. Return (or persist) a clean NO_API_KEY state immediately.
	// This also handles the legacy backward-compat case: older plugin versions
	// never wrote the status option, so it defaulted to 'valid' even when no key
	// was present. With the new NO_API_KEY default in get_status() that edge case
	// is resolved for new installs; the early-return here is the safety net for
	// any sites that still have stale cached data.
	if ( empty( $api_key ) ) {
		$no_key_data = array(
			'currentUserId'  => get_current_user_id(),
			'valid_api_key'  => false,
			'api_key_status' => \RTGODAM\Inc\Enums\Api_Key_Status::NO_API_KEY,
			'user_data'      => array( 'masked_api_key' => '' ),
			'timestamp'      => time(),
		);

		// Persist only if the cache is missing or has an incorrect status.
		if (
			empty( $rtgodam_user_data ) ||
			! isset( $rtgodam_user_data['api_key_status'] ) ||
			\RTGODAM\Inc\Enums\Api_Key_Status::NO_API_KEY !== $rtgodam_user_data['api_key_status']
		) {
			update_option( 'rtgodam_user_data', $no_key_data );
		}

		if ( $use_for_localize_array ) {
			return array(
				'currentUserId' => $no_key_data['currentUserId'],
				'validApiKey'   => false,
				'apiKeyStatus'  => \RTGODAM\Inc\Enums\Api_Key_Status::NO_API_KEY,
				'userApiData'   => $no_key_data['user_data'],
				'timestamp'     => $no_key_data['timestamp'],
			);
		}

		return $no_key_data;
	}

	// Check if we should skip verification.
	// Skip only for expired keys that are past their grace period.
	$skip_verification = false;
	if ( ! $force_refresh && \RTGODAM\Inc\Enums\Api_Key_Status::EXPIRED === $api_key_status && ! rtgodam_is_api_key_in_grace_period() ) {
		// Grace period expired for expired key, pause automatic checks until manual refresh.
		$skip_verification = true;
	}

	$should_verify = (
		empty( $rtgodam_user_data ) ||
		( isset( $rtgodam_user_data['timestamp'] ) && ( time() - $rtgodam_user_data['timestamp'] ) > $timeout )
	) && ! $skip_verification;

	// Allow force refresh to bypass timeout and skip checks.
	if ( $force_refresh ) {
		$should_verify = true;
	}

	if ( $should_verify ) {
		// Verify the user's API Key.
		$result = rtgodam_verify_api_key( $api_key );

		$valid_api_key    = false;
		$user_data        = array();
		$transient_status = null; // For temporary verification_failed state.

		if ( is_wp_error( $result ) ) {
			// API Key shouldn't be invalid if there is a server error.
			$error_data  = $result->get_error_data();
			$status_code = is_array( $error_data ) && isset( $error_data['status'] ) ? $error_data['status'] : \RTGODAM\Inc\Enums\HTTP_Status_Code::INTERNAL_SERVER_ERROR;

			if ( \RTGODAM\Inc\Enums\HTTP_Status_Code::INTERNAL_SERVER_ERROR === $status_code && ! empty( $api_key ) ) {
				// Server error with existing API key - DON'T change DB status, just show verification_failed to user.
				// DON'T set error timestamp - this is temporary and we should keep checking.
				$transient_status = \RTGODAM\Inc\Enums\Api_Key_Status::VERIFICATION_FAILED;
				$valid_api_key    = false;
			} elseif ( \RTGODAM\Inc\Enums\HTTP_Status_Code::INTERNAL_SERVER_ERROR !== $status_code ) {
				$valid_api_key = false;
				// Preserve existing user data for expired/invalid keys.
				$existing_usage = get_option( 'rtgodam-usage', array() );
				if ( ! empty( $existing_usage ) && isset( $existing_usage[ $api_key ] ) ) {
					$user_data = is_object( $existing_usage[ $api_key ] ) ? (array) $existing_usage[ $api_key ] : $existing_usage[ $api_key ];
				}
			}
		} else {
			$valid_api_key = true;
			$user_data     = $result['data'] ?? array();
		}

		$user_data['masked_api_key'] = rtgodam_mask_string( $api_key );

		// Get updated status after verification.
		$api_key_status = rtgodam_get_api_key_status();

		// If there's a transient status (verification_failed), use it instead of DB status.
		if ( ! is_null( $transient_status ) ) {
			$api_key_status = $transient_status;
		}

		$rtgodam_user_data = array(
			'currentUserId'  => get_current_user_id(),
			'valid_api_key'  => $valid_api_key,
			'api_key_status' => $api_key_status,
			'user_data'      => $user_data,
		);

		$usage_data = rtgodam_get_usage_data();

		if ( ! is_wp_error( $usage_data ) ) {
			$rtgodam_user_data = array_merge( $rtgodam_user_data, $usage_data );

			// Check for exceeded limits and set error messages.
			$bandwidth_exceeded = isset( $usage_data['bandwidth_used'], $usage_data['total_bandwidth'] )
				&& $usage_data['bandwidth_used'] > $usage_data['total_bandwidth'];
			$storage_exceeded   = isset( $usage_data['storage_used'], $usage_data['total_storage'] )
				&& $usage_data['storage_used'] > $usage_data['total_storage'];

			if ( $storage_exceeded ) {
				$storage_percentage                         = $usage_data['total_storage'] > 0
					? number_format( ( $usage_data['storage_used'] / $usage_data['total_storage'] ) * 100, 1 )
					: '0';
				$rtgodam_user_data['storageBandwidthError'] = sprintf(
					/* translators: %s: storage usage percentage */
					__( 'Storage limit exceeded (%s%%). Please upgrade your plan to continue.', 'godam' ),
					$storage_percentage
				);
			} elseif ( $bandwidth_exceeded ) {
				$bandwidth_percentage                       = $usage_data['total_bandwidth'] > 0
					? number_format( ( $usage_data['bandwidth_used'] / $usage_data['total_bandwidth'] ) * 100, 1 )
					: '0';
				$rtgodam_user_data['storageBandwidthError'] = sprintf(
					/* translators: %s: bandwidth usage percentage */
					__( 'Bandwidth limit exceeded (%s%%). Please upgrade your plan to continue.', 'godam' ),
					$bandwidth_percentage
				);
			}
		} elseif ( is_wp_error( $usage_data ) && $valid_api_key ) {
			// Only show usage data fetch error if API key is valid.
			// API key errors are handled separately via apiKeyStatus.
			$rtgodam_user_data['storageBandwidthError'] = $usage_data->get_error_message();
		}

		$rtgodam_user_data['timestamp'] = time();

		// Save the userData in wp_options.
		update_option( 'rtgodam_user_data', $rtgodam_user_data );
	}

	// Ensure api_key_status is always present in the data.
	if ( ! isset( $rtgodam_user_data['api_key_status'] ) ) {
		$rtgodam_user_data['api_key_status'] = rtgodam_get_api_key_status();
	}

	if ( $use_for_localize_array ) {
		// Prepare the data for localizing scripts.
		$localized_array_data = array(
			'currentUserId' => $rtgodam_user_data['currentUserId'],
			'validApiKey'   => $rtgodam_user_data['valid_api_key'],
			'apiKeyStatus'  => $rtgodam_user_data['api_key_status'],
			'userApiData'   => $rtgodam_user_data['user_data'],
			'timestamp'     => $rtgodam_user_data['timestamp'],
		);

		if ( isset( $rtgodam_user_data['storageBandwidthError'] ) && ! empty( $rtgodam_user_data['storageBandwidthError'] ) ) {
			$localized_array_data['storageBandwidthError'] = $rtgodam_user_data['storageBandwidthError'];
		}

		// Use isset() instead of !empty() to allow 0 values.
		if ( isset( $rtgodam_user_data['storage_used'] ) ) {
			$localized_array_data['storageUsed'] = $rtgodam_user_data['storage_used'];
		}

		if ( isset( $rtgodam_user_data['total_storage'] ) ) {
			$localized_array_data['totalStorage'] = $rtgodam_user_data['total_storage'];
		}

		if ( isset( $rtgodam_user_data['bandwidth_used'] ) ) {
			$localized_array_data['bandwidthUsed'] = $rtgodam_user_data['bandwidth_used'];
		}

		if ( isset( $rtgodam_user_data['total_bandwidth'] ) ) {
			$localized_array_data['totalBandwidth'] = $rtgodam_user_data['total_bandwidth'];
		}

		return $localized_array_data;
	}

	return $rtgodam_user_data;
}

/**
 * Get the storage and bandwidth usage data.
 *
 * @return array|WP_Error
 */
function rtgodam_get_usage_data() {

	$api_key = get_option( 'rtgodam-api-key', '' );

	if ( empty( $api_key ) ) {
		return new \WP_Error( 'rtgodam_api_error', __( 'API key not found ( try refreshing the page )', 'godam' ) );
	}

	$endpoint = RTGODAM_API_BASE . '/api/method/godam_core.api.stats.get_bandwidth_and_storage';

	// Prepare request body with API key.
	$request_body = array(
		'api_key' => $api_key,
	);

	$args = array(
		'body'    => wp_json_encode( $request_body ),
		'headers' => array(
			'Content-Type' => 'application/json',
		),
	);

	// Use vip_safe_wp_remote_post as primary and wp_safe_remote_post as fallback.
	if ( function_exists( 'vip_safe_wp_remote_post' ) ) {
		$response = vip_safe_wp_remote_post( $endpoint, $args, 3, 3 );
	} else {
		$response = wp_safe_remote_post( $endpoint, $args );
	}

	if ( is_wp_error( $response ) ) {
		return $response;
	}

	$body = wp_remote_retrieve_body( $response );

	$data = json_decode( $body, true );

	// Validate response structure.
	if ( ! isset( $data['message'] ) || ! isset( $data['message']['storage_used'] ) || ! isset( $data['message']['bandwidth_used'] ) ) {
		return new \WP_Error( 'rtgodam_api_error', __( 'Error fetching data for storage and bandwidth ( remove and add again the API key to get usage analytics )', 'godam' ) );
	}

	return array(
		'storage_used'    => floatval( $data['message']['storage_used'] ?? 0 ),
		'total_storage'   => floatval( $data['message']['total_storage'] ?? 0 ),
		'bandwidth_used'  => floatval( $data['message']['bandwidth_used'] ?? 0 ),
		'total_bandwidth' => floatval( $data['message']['total_bandwidth'] ?? 0 ),
	);
}

/**
 * Check if the api key is valid.
 *
 * @return bool
 */
function rtgodam_is_api_key_valid() {
	$user_data = rtgodam_get_user_data();

	return ! empty( $user_data['valid_api_key'] ) ? true : false;
}

/**
 * Checks if the given filename is an audio file based on its name.
 *
 * Note: The files created by uppy webcam, screen capture, and audio plugin are in the same format. So we are checking the filename to determine if it's an audio file.
 *
 * @since 1.4.1
 *
 * @param string $filename The name of the file to check.
 *
 * @return bool True if the file is an audio file, false otherwise.
 */
function godam_is_audio_file_by_name( $filename ) {
	// Extract only the basename (ignores full path).
	$basename = basename( $filename );

	// Check if 'audio' appears in the filename (case-insensitive).
	return stripos( $basename, 'audio' ) !== false;
}

/**
 * Check if the given file is an audio file.
 *
 * @since 1.6.0
 *
 * @param string $file_path_or_url The file path or URL to check.
 *
 * @return bool True if the file is an audio file, false otherwise.
 */
function godam_is_audio_file( $file_path_or_url ) {
	if ( empty( $file_path_or_url ) || ! is_string( $file_path_or_url ) ) {
		return false;
	}

	$file_type = wp_check_filetype( $file_path_or_url );
	$mime_type = ! empty( $file_type['type'] ) ? $file_type['type'] : '';

	// Check if the MIME type indicates an audio file.
	if ( ! empty( $mime_type ) && str_starts_with( $mime_type, 'audio/' ) ) {
		return true;
	}

	// Container formats that can hold both audio and video.
	// These require filename-based detection to distinguish audio from video.
	$container_formats = array( 'webm', 'mp4' );

	// Handle ambiguous container formats that might be audio.
	// Browser-specific behavior:
	// - Chromium (Chrome, Edge): Saves both video and audio as .webm
	// - Firefox: Saves audio as .ogg, video as .webm
	// - Safari: Saves both audio and video as .mp4
	// We check the filename for 'audio' keyword to determine if it's an audio file.
	if ( in_array( $file_type['ext'], $container_formats, true ) && godam_is_audio_file_by_name( $file_path_or_url ) ) {
		return true;
	}

	return false;
}

/**
 * Send Video file to GoDAM for transcoding.
 *
 * @param string  $form_type  Form Type.
 * @param string  $form_title Form Title.
 * @param string  $file_url   File URL.
 * @param integer $entry_id   Entry Id.
 * @param string  $job_type   Job type Default is 'stream'.
 *
 * @return array|WP_Error
 */
function rtgodam_send_video_to_godam_for_transcoding( $form_type = '', $form_title = '', $file_url = '', $entry_id = 0, $job_type = 'stream' ) {

	/**
	 * Filter to allow external developers to disable automatic transcoding for form uploads.
	 * This allows clients to have manual control over when form-recorded videos get transcoded.
	 *
	 * This is the same filter used for media library uploads, providing unified control.
	 * When disabled, form submissions will fail with an error message indicating transcoding is disabled.
	 * Manual retranscoding via the admin interface will still work regardless of this setting.
	 *
	 * Example usage:
	 * add_filter( 'godam_auto_transcode_on_upload', '__return_false' ); // Disable globally
	 *
	 * @since 1.5.0
	 *
	 * @param bool $auto_transcode_on_upload Whether to automatically transcode form uploads. Default true.
	 */
	if ( ! apply_filters( 'godam_auto_transcode_on_upload', true ) ) {
		return new WP_Error(
			'transcoding_disabled',
			__( 'Form transcoding has been disabled by the site administrator.', 'godam' )
		);
	}

	/**
	 * Extract file extension.
	 */
	$file_extension = pathinfo( $file_url, PATHINFO_EXTENSION );

	/**
	 * Get MIME type from file URL.
	 */
	$file_type_info = wp_check_filetype( $file_url );
	$content_type   = $file_type_info['type'] ?? '';

	/**
	 * Set the default settings.
	 */
	$default_settings = array(
		'video' => array(
			'adaptive_bitrate'    => true,
			'watermark'           => false,
			'watermark_text'      => '',
			'watermark_url'       => '',
			'video_thumbnails'    => 0,
			'use_watermark_image' => false,
		),
	);

	/**
	 * Fetch Godam site settings.
	 */
	$godam_settings = get_option( 'rtgodam-settings', $default_settings );

	/**
	 * Fetch stored API Key.
	 */
	$api_key = get_option( 'rtgodam-api-key', '' );

	/**
	 * Watermark settings.
	 */
	$rtgodam_watermark           = $godam_settings['video']['watermark'];
	$rtgodam_use_watermark_image = $godam_settings['video']['use_watermark_image'];
	$rtgodam_watermark_text      = sanitize_text_field( $godam_settings['video']['watermark_text'] );
	$rtgodam_watermark_url       = esc_url( $godam_settings['video']['watermark_url'] );

	$watermark_to_use = array();

	/**
	 * Include watermark if set.
	 */
	if ( $rtgodam_watermark && $rtgodam_use_watermark_image ) {

		if ( ! empty( $rtgodam_watermark_url ) ) {
			$watermark_to_use['watermark_url'] = $rtgodam_watermark_url;
		}

		if ( ! empty( $rtgodam_watermark_text ) ) {
			$watermark_to_use['watermark_text'] = $rtgodam_watermark_text;
		}
	}

	/**
	 * Callback URL from CMM to plugin for transcoding.
	 */
	$callback_url = rest_url( 'godam/v1/transcoder-callback' );

	/**
	 * Manually setting the rest api endpoint, we can refactor that later to use similar functionality as callback_url.
	 */
	$status_callback_url = get_rest_url( get_current_blog_id(), '/godam/v1/transcoding/transcoding-status' );

	/**
	 * Prepare data to send as post request to CMM.
	 */
	// Get current user information for form submissions (since no specific attachment).
	$current_user = wp_get_current_user();
	$site_url     = get_site_url();

	// Get author name with fallback to username.
	$author_first_name = $current_user->first_name ?? '';
	$author_last_name  = $current_user->last_name ?? '';
	$author_email      = $current_user->user_email ?? '';

	// If first and last names are empty, use username as fallback.
	if ( empty( $author_first_name ) && empty( $author_last_name ) ) {
		$author_first_name = $current_user->user_login ?? '';
	}

	$body = array_merge(
		array(
			'api_token'            => $api_key,
			'job_type'             => $job_type ?? 'stream',
			'job_for'              => ! empty( $form_type ) ? $form_type . '-godam-recorder' : 'gf-godam-recorder',
			'file_origin'          => rawurlencode( $file_url ),
			'callback_url'         => rawurlencode( $callback_url ),
			'status_callback'      => rawurlencode( $status_callback_url ),
			'force'                => 0,
			'formats'              => $file_extension,
			'thumbnail_count'      => 0,
			'stream'               => true,
			'watermark'            => boolval( $rtgodam_watermark ),
			'resolutions'          => array( 'auto' ),
			'mime_type'            => $content_type,
			'folder_name'          => ! empty( $form_title ) ? $form_title : __( 'Gravity forms', 'godam' ),
			'wp_author_email'      => apply_filters( 'godam_author_email_to_send', $author_email, 0 ),
			'wp_site'              => $site_url,
			'wp_author_first_name' => apply_filters( 'godam_author_first_name_to_send', $author_first_name, 0 ),
			'wp_author_last_name'  => apply_filters( 'godam_author_last_name_to_send', $author_last_name, 0 ),
			'public'               => 1,
		),
		$watermark_to_use
	);

	/**
	 * Prepare the args to pass to request.
	 */
	$args = array(
		'method'    => 'POST',
		'sslverify' => false,
		'timeout'   => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
		'body'      => $body,
	);

	/**
	 * CMM Api key construction.
	 */
	$transcoding_api_url = RTGODAM_API_BASE . '/api/';
	$transcoding_url     = $transcoding_api_url . 'resource/Transcoder Job';

	/**
	 * Response from post request.
	 */
	$response = wp_remote_post( $transcoding_url, $args );

	if ( is_wp_error( $response ) || empty( $response['response']['code'] ) || 200 !== intval( $response['response']['code'] ) ) {
		return new WP_Error(
			400,
			sprintf(
				/* translators: %s: Entry ID for which transcoding failed */
				__( 'Transcoding failed | entry Id: %s', 'godam' ),
				$entry_id
			)
		);
	}

	return json_decode( $response['body'] );
}

/**
 * Format video duration based on the selected format for GoDAM block.
 *
 * @param string $duration        The raw duration value in seconds.
 * @param string $duration_format The format to use (default, minutes, seconds).
 *
 * @return string The formatted duration string.
 */
function rtgodam_block_format_video_duration( $duration, $duration_format = 'default' ) {
	if ( empty( $duration ) ) {
		return '';
	}

	// Parse the duration - assuming it's stored in seconds.
	$total_seconds = intval( $duration );
	$hours         = floor( $total_seconds / 3600 );
	$minutes       = floor( ( $total_seconds % 3600 ) / 60 );
	$seconds       = $total_seconds % 60;

	switch ( $duration_format ) {
		case 'minutes':
			// Show as MM:SS.
			$total_minutes = floor( $total_seconds / 60 );
			return sprintf( '%02d:%02d', $total_minutes, $seconds );

		case 'seconds':
			// Show total seconds with 's' suffix.
			return $total_seconds . __( 's', 'godam' );

		case 'default':
		default:
			// Show as HH:MM:SS.
			return sprintf( '%02d:%02d:%02d', $hours, $minutes, $seconds );
	}
}

/**
 * Retrieves user data for the current session.
 *
 * This function checks if a user is logged in and returns their email and display name.
 * If no user is logged in, it checks for a guest user cookie and returns the guest user's
 * email and constructed name. If neither is available, it defaults to a bot email and
 * guest name.
 *
 * @return array An associative array containing 'email' and 'name' of the user or guest.
 */
function rtgodam_get_current_logged_in_user_data() {
	if ( is_user_logged_in() ) {
		$current_user = wp_get_current_user();
		return array(
			'email' => $current_user->user_email,
			'name'  => $current_user->display_name,
			'type'  => 'user',
		);
	}

	// @To-do: Check for guest user cookie when we introduce this feature.

	$domain = preg_replace( '/^www\./', '', wp_parse_url( home_url(), PHP_URL_HOST ) );
	return array(
		'email' => 'anonymous@' . $domain,
		'name'  => __( 'Anonymous', 'godam' ),
		'type'  => 'non-user',
	);
}

/**
 * Retrieves a cached value by key.
 *
 * This function checks if the external object cache is being used.
 * If so, it retrieves the cached value using the WordPress cache API.
 * Otherwise, it retrieves the value from the transient API.
 *
 * @param string $key The cache key to retrieve the value for.
 * @return mixed The cached value, or false if the value does not exist.
 */
function rtgodam_cache_get( $key ) {
	if ( wp_using_ext_object_cache() ) {
		return wp_cache_get( $key );
	} else {
		return get_transient( $key );
	}
}

/**
 * Sets a value in the cache for a given key.
 *
 * This function checks if the external object cache is being used.
 * If so, it sets the value using the WordPress cache API.
 * Otherwise, it sets the value using the transient API.
 *
 * @param string $key     The cache key to set the value for.
 * @param mixed  $value   The value to set.
 * @param int    $expiration Optional. The time until the value expires in seconds.
 *
 * @return bool True on success, false on failure.
 */
function rtgodam_cache_set( $key, $value, $expiration = 900 ) {
	if ( is_string( $expiration ) || $expiration < 300 ) {
		$expiration = 300;
	}
	if ( wp_using_ext_object_cache() ) {
		return wp_cache_set( $key, $value, '', $expiration ); // phpcs:ignore
	} else {
		return set_transient( $key, $value, $expiration );
	}
}

/**
 * Deletes a cached value by key.
 *
 * This function checks if the external object cache is being used.
 * If so, it deletes the cached value using the WordPress cache API.
 * Otherwise, it deletes the value using the transient API.
 *
 * @param string $key The cache key to delete the value for.
 * @return bool True on success, false on failure.
 */
function rtgodam_cache_delete( $key ) {
	if ( wp_using_ext_object_cache() ) {
		return wp_cache_delete( $key );
	} else {
		return delete_transient( $key );
	}
}

/**
 * Check if the current environment is localhost.
 *
 * This function checks the server's remote address and host to determine if the site is running in a local development environment.
 * It checks against a whitelist of common localhost IPs and also looks for '.local' or '.test' in the host name.
 * Additionally, it respects the RTGODAM_IS_LOCAL constant if defined.
 *
 * @since 1.4.3
 *
 * @return bool True if the environment is localhost, false otherwise.
 */
function rtgodam_is_local_environment() {

	$whitelist = array( '127.0.0.1', '::1', 'localhost' );

	// phpcs:disable -- Disabling phpcs as its not manipulating any data, just reading server variables, and function is used for local environment check only.
	// We do NOT use REMOTE_ADDR because it can be 127.0.0.1 for loopback requests (Server processes) on hosted sites.
	// Instead, we rely on HTTP_HOST to detect if the site is actually hosted on localhost/local domains.
	$host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
	// phpcs:enable

	// Strip port number if present (e.g., "localhost:8080" → "localhost").
	$host_without_port = preg_replace( '/:\d+$/', '', $host );

	$is_localhost = (
		in_array( $host_without_port, $whitelist, true ) ||
		preg_match( '/\.local$/', $host_without_port ) ||
		preg_match( '/\.test$/', $host_without_port )
	);

	return ( $is_localhost || ( defined( 'RTGODAM_IS_LOCAL' ) && RTGODAM_IS_LOCAL ) );
}

/**
 * Check if the site has HTTP authentication enabled.
 *
 * This function uses a result from JavaScript-based detection.
 * The JS detection runs in the browser and can accurately detect HTTP auth
 * by making a request without credentials, then stores the result in an option.
 * If no cached status exists, this function falls back to a default of "not enabled".
 *
 * @since 1.7.1
 *
 * @return bool True if HTTP auth is enforced, false otherwise.
 */
function rtgodam_has_http_auth(): bool {
	$has_http_auth = false;

	// Get detection result from JavaScript detector.
	$cached_status = get_option( 'rtgodam_http_auth_status', array() );

	// If we have a recent detection result, use it.
	if ( ! empty( $cached_status ) && isset( $cached_status['enabled'] ) ) {
		$has_http_auth = (bool) $cached_status['enabled'];
	}

	/**
	 * Filter to override HTTP auth detection.
	 *
	 * @since 1.7.1
	 *
	 * @param bool $has_http_auth Whether HTTP auth is enforced.
	 */
	return apply_filters( 'godam_has_http_auth', $has_http_auth );
}

/**
 * Fetch AI-generated video transcript path.
 *
 * @param int         $attachment_id The attachment ID (must be numeric).
 * @param string|null $job_id        Optional. The transcription job ID. If not provided, will be retrieved from post meta.
 * @return string|false Transcript path if available, false otherwise.
 */
function godam_get_transcript_path( $attachment_id, $job_id = null ) {
	if ( empty( $attachment_id ) || ! is_numeric( $attachment_id ) ) {
		return false;
	}

	// Check post meta first.
	$transcript_path = get_post_meta( $attachment_id, 'rtgodam_transcript_path', true );
	if ( ! empty( $transcript_path ) ) {
		return $transcript_path;
	}

	// Get job_id from parameter or post meta.
	if ( empty( $job_id ) ) {
		$job_id = get_post_meta( $attachment_id, 'rtgodam_transcoding_job_id', true );
		if ( empty( $job_id ) ) {
			$job_id = get_post_meta( $attachment_id, '_godam_original_id', true );
		}
	}

	if ( empty( $job_id ) ) {
		return false;
	}

	// Get API key.
	$api_key = get_option( 'rtgodam-api-key', '' );
	if ( empty( $api_key ) ) {
		return false;
	}

	// Make POST request to Frappe API.
	$rest_url = RTGODAM_API_BASE . '/api/method/godam_core.api.process.get_transcription';

	$request_body = array(
		'job_name' => sanitize_text_field( $job_id ),
		'api_key'  => sanitize_text_field( $api_key ),
	);

	$args = array(
		'method'  => 'POST',
		'timeout' => 3,
		'headers' => array(
			'Content-Type' => 'application/json',
		),
		'body'    => wp_json_encode( $request_body ),
	);

	$response = wp_remote_post( $rest_url, $args );

	if ( is_wp_error( $response ) ) {
		return false;
	}

	$response_code = wp_remote_retrieve_response_code( $response );

	if ( 200 !== $response_code ) {
		return false;
	}

	$body = wp_remote_retrieve_body( $response );
	$data = json_decode( $body, true );

	if (
		is_array( $data ) &&
		isset( $data['message']['transcript_path'], $data['message']['transcription_status'] ) &&
		'Transcribed' === $data['message']['transcription_status']
	) {
		$transcript_path = esc_url_raw( $data['message']['transcript_path'] );

		// Save to post meta using the attachment ID.
		if ( ! empty( $transcript_path ) ) {
			update_post_meta( $attachment_id, 'rtgodam_transcript_path', $transcript_path );
		}

		return $transcript_path;
	}

	return false;
}

/**
 * Generate the HTML content for the video preview page.
 *
 * This function constructs the HTML structure for a video preview page based on the provided video ID.
 * It checks if the video exists and displays either the video player or an error message accordingly.
 *
 * @param int $video_id The ID of the video attachment to preview.
 * @return string The generated HTML content for the video preview page.
 */
function godam_preview_page_content( $video_id ) {
	ob_start();
	// Check if video ID is provided and if video attachment exists.
	$video_attachment = null;
	$show_video       = false;
	$video_id         = intval( $video_id );

	if ( ! empty( $video_id ) ) {
		$video_attachment = get_post( $video_id );
		$show_video       = $video_attachment && 'attachment' === $video_attachment->post_type;
	}

	if ( ! $show_video ) {
		// Display error message for missing or invalid video.
		?>
		<div class="godam-video-preview--container">
			<p class="video-not-found"><?php esc_html_e( 'Oops! We could not locate your video', 'godam' ); ?></p>
		</div>
		<?php
	} else {
		// Display video content.
		?>
		<div class="godam-video-preview--notice">
			<?php esc_html_e( 'Note: This is a simple video preview. The video player may display differently when added to a page based on theme styles.', 'godam' ); ?>
		</div>
		<div class="godam-video-preview">
			<h1 class="godam-video-preview--title">
				<?php echo esc_html( get_the_title( $video_id ) ); ?>
			</h1>
			<?php echo do_shortcode( '[godam_video id="' . $video_id . '"]' ); ?>
		</div>
		<?php
	}
	return ob_get_clean();
}

/**
 * Get post id from meta key and value.
 *
 * @since 1.5.0
 *
 * @param string $key   Meta key.
 * @param mixed  $value Meta value.
 *
 * @return int|bool     Return post id if found else false.
 */
function rtgodam_get_post_id_by_meta_key_and_value( $key, $value ) {
	global $wpdb;
	$cache_key = md5( 'meta_key_' . $key . '_meta_value_' . $value );

	$meta = rtgodam_cache_get( $cache_key );
	if ( empty( $meta ) ) {
		$meta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value = %s", $key, $value ) );  // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		rtgodam_cache_set( $cache_key, $meta, HOUR_IN_SECONDS );
	}

	if ( is_array( $meta ) && ! empty( $meta ) && isset( $meta[0] ) ) {
		$meta = $meta[0];
	}
	if ( is_object( $meta ) ) {
		return $meta->post_id;
	}
	return false;
}

/**
 * Check whether the engagement feature is available in GoDAM.
 *
 * Returning true enables engagement settings and runtime behavior for
 * GoDAM video, gallery, and embed experiences.
 *
 * @since 1.8.0
 *
 * @return bool Whether the engagement feature is enabled.
 */
function rtgodam_is_engagement_feature_enabled() {
	/**
	 * Filters whether the GoDAM engagement feature is enabled.
	 *
	 * Return true to enable engagement settings and functionality for GoDAM
	 * videos, galleries, and embeds.
	 *
	 * @since 1.8.0
	 *
	 * @param bool $is_enabled Whether the engagement feature is enabled. Default false.
	 */
	return (bool) apply_filters( 'rtgodam_enable_engagement_feature', false );
}

/**
 * Generate HTML content for the video embed page.
 *
 * This function produces the HTML markup for embedding a single video.
 * It displays the video player only, without any headers or notices,
 * making it suitable for embedding in iframes or modals.
 *
 * @param int    $video_id      The ID of the video attachment to embed.
 * @param string $godam_context Optional. The player context passed to the godam_video shortcode (e.g. 'video-only').
 * @param string $bg_color      Optional. Hex background color applied as --godam-video-bg-color CSS variable on the wrapper.
 * @param bool   $show_engagements Optional. Whether to show engagements in the embed. Default false (no engagements shown).
 *
 * @since 1.5.0
 *
 * @return string The generated HTML content for the video embed page.
 */
function godam_embed_page_content( $video_id, $godam_context = '', $bg_color = '', $show_engagements = false ) {
	ob_start();
	// Check if video ID is provided and if video attachment exists.
	$video_attachment  = null;
	$show_video        = false;
	$video_id          = intval( $video_id );
	$engagements_value = rtgodam_is_engagement_feature_enabled() && $show_engagements ? 'show' : '';

	if ( ! empty( $video_id ) ) {
		$video_attachment = get_post( $video_id );
		$show_video       = $video_attachment && 'attachment' === $video_attachment->post_type && 'video/' === substr( $video_attachment->post_mime_type, 0, 6 );
	}

	if ( ! $show_video ) {
		// Display error message for missing or invalid video.
		?>
		<div class="godam-video-embed--container">
			<p class="video-not-found"><?php esc_html_e( 'Video not found', 'godam' ); ?></p>
		</div>
		<?php
	} else {
		// Display video content.
		$godam_shortcode = '[godam_video id="' . $video_id . '"';
		if ( ! empty( $godam_context ) ) {
			$godam_shortcode .= ' godam_context="' . esc_attr( $godam_context ) . '"';
		}
		if ( ! empty( $engagements_value ) ) {
			$godam_shortcode .= ' engagements="' . esc_attr( $engagements_value ) . '"';
		}
		$godam_shortcode .= ']';

		$godam_wrapper_style = ! empty( $bg_color ) ? '--godam-video-bg-color: ' . $bg_color . ';' : '';
		?>
		<div 
			class="godam-video-embed" data-godam-context="<?php echo esc_attr( $godam_context ); ?>"
			data-show-engagements="<?php echo esc_attr( $show_engagements ? 'true' : 'false' ); ?>"
			style="<?php echo esc_attr( $godam_wrapper_style ); ?>"
		>
			<?php echo do_shortcode( $godam_shortcode ); ?>
		</div>
		<?php
	}
	return ob_get_clean();
}

/**
 * Convert one or more URLs to HTTPS if the current page is using SSL.
 *
 * This function checks whether the current page is using SSL and,
 * if so, returns the given URL string or array of URLs with their scheme changed to HTTPS.
 * If SSL is not active, the original value is returned unchanged.
 *
 * @since 1.7.0
 *
 * @param array|string $urls The URLs to change the scheme of.
 *
 * @return array|string The URLs with the scheme changed to HTTPS.
 */
function rtgodam_convert_to_https_url( $urls ) {

	if ( ! is_ssl() ) {
		return $urls;
	}

	if ( is_array( $urls ) ) {

		$filtered_urls = array_filter(
			$urls,
			function ( $url ) {
				return null !== $url && '' !== $url;
			}
		);

		$converted_urls = array_map(
			function ( $url ) {
				return set_url_scheme( $url, 'https' );
			},
			$filtered_urls
		);

		return array_filter(
			$converted_urls,
			function ( $url ) {
				return null !== $url && '' !== $url;
			}
		);
	}

	if ( null === $urls || '' === $urls ) {
		return $urls;
	}

	return set_url_scheme( $urls, 'https' );
}

/**
 * Check if auth detector scripts should be loaded on current screen.
 *
 * @param WP_Screen|null $screen Current screen object.
 *
 * @return bool True if auth detector scripts should load.
 */
function godam_should_load_auth_detector_script( $screen ) {
	if ( ! $screen ) {
		return false;
	}

	// Pages where media library/modal is directly accessible.
	$media_screens = array(
		'upload',     // Media Library page (grid & list views).
		'post',       // Add/Edit Post.
		'page',       // Add/Edit Page.
		'attachment', // Edit Media page.
	);

	// Check if current screen is in the list.
	if ( in_array( $screen->base, $media_screens, true ) || in_array( $screen->id, $media_screens, true ) ) {
		return true;
	}

	// Check if on GoDAM admin pages (where media library/modal can be opened).
	$godam_pages = array(
		'toplevel_page_godam',             // Dashboard page.
		'godam_page_rtgodam_video_editor', // Video Editor page.
	);

	if ( in_array( $screen->id, $godam_pages, true ) ) {
		return true;
	}

	return false;
}

// ---------------------------------------------------------------------------
// Work-cache helpers
// ---------------------------------------------------------------------------

/** Cache group used across all work-cache entries. */
if ( ! defined( 'RTGODAM_WORK_CACHE_GROUP' ) ) {
	define( 'RTGODAM_WORK_CACHE_GROUP', 'rtgodam_work_cache' );
}

/** Cache version — bump to globally invalidate all work-cache entries. */
if ( ! defined( 'RTGODAM_WORK_CACHE_VERSION' ) ) {
	define( 'RTGODAM_WORK_CACHE_VERSION', 'v1' );
}

/** Default TTL (seconds) used as hard-expiry fallback: 30 minutes. */
if ( ! defined( 'RTGODAM_WORK_CACHE_TTL' ) ) {
	define( 'RTGODAM_WORK_CACHE_TTL', 30 * MINUTE_IN_SECONDS );
}

/**
 * Retrieve a value from the work cache.
 *
 * Uses the WordPress object cache when an external cache is active,
 * otherwise uses transients.
 *
 * @since 1.8.0
 *
 * @param string $key Cache key (without version prefix).
 * @return mixed|false Cached value or false on miss.
 */
function rtgodam_work_cache_get( $key ) {
	$versioned_key = RTGODAM_WORK_CACHE_VERSION . '_' . $key;

	if ( wp_using_ext_object_cache() ) {
		return wp_cache_get( $versioned_key, RTGODAM_WORK_CACHE_GROUP );
	}

	return get_transient( 'rtgodam_wc_' . md5( $versioned_key ) );
}

/**
 * Store a value in the work cache.
 *
 * Uses the WordPress object cache when an external cache is active,
 * otherwise uses transients.
 *
 * @since 1.8.0
 *
 * @param string $key   Cache key (without version prefix).
 * @param mixed  $value Value to cache.
 * @param int    $ttl   Time-to-live in seconds. Defaults to RTGODAM_WORK_CACHE_TTL.
 */
function rtgodam_work_cache_set( $key, $value, $ttl = RTGODAM_WORK_CACHE_TTL ) {
	$versioned_key = RTGODAM_WORK_CACHE_VERSION . '_' . $key;

	if ( wp_using_ext_object_cache() ) {
		// phpcs:ignore WordPressVIPMinimum.Performance.LowExpiryCacheTime.CacheTimeUndetermined -- $ttl defaults to RTGODAM_WORK_CACHE_TTL (1800s) and callers must pass >= 300s.
		wp_cache_set( $versioned_key, $value, RTGODAM_WORK_CACHE_GROUP, $ttl );
	} else {
		set_transient( 'rtgodam_wc_' . md5( $versioned_key ), $value, $ttl );
	}
}

/**
 * Delete a single entry from the work cache.
 *
 * Clears both the dedicated object-cache entry and the transient key so
 * stale data is removed even if the site's cache backend changed.
 *
 * @since 1.8.0
 *
 * @param string $key Cache key (without version prefix).
 */
function rtgodam_work_cache_delete( $key ) {
	$versioned_key = RTGODAM_WORK_CACHE_VERSION . '_' . $key;

	wp_cache_delete( $versioned_key, RTGODAM_WORK_CACHE_GROUP );
	delete_transient( 'rtgodam_wc_' . md5( $versioned_key ) );
}

/**
 * Register a cache key under an index so it can be bulk-deleted later.
 *
 * The index is stored as a transient with a TTL aligned to the maximum
 * render-cache TTL (RTGODAM_WORK_CACHE_TTL). This ensures stale keys are
 * pruned naturally when the index expires rather than accumulating indefinitely
 * in permanent options.
 *
 * If a key is already registered under the index the TTL of the transient is
 * refreshed so the index stays alive as long as any of its members could still
 * be cached.
 *
 * @since 1.8.0
 *
 * @param string $index_key Human-readable index identifier (e.g. `work_cache_godam_meta_{post_id}`).
 * @param string $cache_key The cache key to register.
 */
function rtgodam_work_cache_index_add( $index_key, $cache_key ) {
	$transient_name = 'rtgodam_wc_idx_' . md5( $index_key );
	$members        = (array) get_transient( $transient_name );

	if ( ! in_array( $cache_key, $members, true ) ) {
		$members[] = $cache_key;
	}

	// Always refresh the TTL so the index outlives the youngest member.
	set_transient( $transient_name, $members, RTGODAM_WORK_CACHE_TTL );
}

/**
 * Return all cache keys registered under an index.
 *
 * Returns an empty array when the index transient has expired, which means
 * all previously registered cache entries have also naturally expired.
 *
 * @since 1.8.0
 *
 * @param string $index_key Index identifier.
 * @return string[] List of registered cache keys.
 */
function rtgodam_work_cache_index_members( $index_key ) {
	$members = get_transient( 'rtgodam_wc_idx_' . md5( $index_key ) );
	return is_array( $members ) ? $members : array();
}

/**
 * Delete every cache key registered under an index and remove the index transient.
 *
 * @since 1.8.0
 *
 * @param string $index_key Index identifier.
 */
function rtgodam_work_cache_index_clear( $index_key ) {
	$members = rtgodam_work_cache_index_members( $index_key );

	foreach ( $members as $cache_key ) {
		rtgodam_work_cache_delete( $cache_key );
	}

	delete_transient( 'rtgodam_wc_idx_' . md5( $index_key ) );
}

// ---------------------------------------------------------------------------

/**
 * Normalize a GoDAM video performance mode value.
 *
 * @since 1.8.0
 *
 * @param string $mode     Candidate performance mode.
 * @param string $fallback Fallback mode when the candidate is invalid.
 *
 * @return string
 */
function rtgodam_normalize_video_performance_mode( $mode, $fallback = 'balanced' ) {
	$mode     = is_string( $mode ) ? sanitize_key( $mode ) : '';
	$fallback = 'priority' === sanitize_key( $fallback ) ? 'priority' : 'balanced';

	if ( in_array( $mode, array( 'balanced', 'priority' ), true ) ) {
		return $mode;
	}

	return $fallback;
}

/**
 * Resolve the effective performance mode for a video from modern or legacy attributes.
 *
 * @since 1.8.0
 *
 * @param array  $attributes Block or shortcode attributes.
 * @param string $default_mode Default performance mode when no stored value exists.
 *
 * @return string
 */
function rtgodam_resolve_video_performance_mode( $attributes, $default_mode = 'balanced' ) {
	if ( ! is_array( $attributes ) ) {
		return rtgodam_normalize_video_performance_mode( '', $default_mode );
	}

	if ( ! empty( $attributes['performanceMode'] ) ) {
		return rtgodam_normalize_video_performance_mode( $attributes['performanceMode'], $default_mode );
	}

	if ( ! empty( $attributes['performance_mode'] ) ) {
		return rtgodam_normalize_video_performance_mode( $attributes['performance_mode'], $default_mode );
	}

	$legacy_preload = isset( $attributes['preload'] ) ? strtolower( trim( (string) $attributes['preload'] ) ) : '';

	if ( in_array( $legacy_preload, array( 'metadata', 'auto', 'none' ), true ) ) {
		return 'balanced';
	}

	if ( in_array( $legacy_preload, array( 'preload only video thumbnail' ), true ) ) {
		return 'priority';
	}

	if ( ! empty( $attributes['preloadPoster'] ) ) {
		return 'priority';
	}

	return rtgodam_normalize_video_performance_mode( '', $default_mode );
}

/**
 * Resolve the final performance-driven render settings for a single video.
 *
 * @since 1.8.0
 *
 * @param array  $attributes Block or shortcode attributes.
 * @param string $default_mode Default performance mode.
 *
 * @return array<string, mixed>
 */
function rtgodam_get_video_performance_settings( $attributes, $default_mode = 'balanced' ) {
	$mode = rtgodam_resolve_video_performance_mode( $attributes, $default_mode );

	return array(
		'mode'              => $mode,
		'video_attributes'  => array(
			'preload' => 'priority' === $mode ? 'metadata' : 'none',
		),
		'poster_attributes' => 'priority' === $mode
			? array(
				'fetchpriority' => 'high',
			)
			: array(
				'loading' => 'lazy',
			),
	);
}

/**
 * Resolve the render attributes for a gallery tile thumbnail.
 *
 * Priority mode is intentionally capped to the leading tiles to avoid over-eager
 * image loading in multi-video layouts.
 *
 * @since 1.8.0
 *
 * @param string $performance_mode Requested performance mode.
 * @param int    $index            Zero-based tile index.
 * @param int    $priority_cutoff  Number of leading tiles that may stay in priority mode.
 *
 * @return array<string, string>
 */
function rtgodam_get_gallery_tile_image_attributes( $performance_mode, $index = 0, $priority_cutoff = 3 ) {
	$performance_mode = rtgodam_normalize_video_performance_mode( $performance_mode );
	$index            = max( 0, intval( $index ) );
	$priority_cutoff  = max( 0, intval( $priority_cutoff ) );
	$is_priority_tile = 'priority' === $performance_mode && $index < $priority_cutoff;

	if ( $is_priority_tile ) {
		return array(
			'fetchpriority' => 'high',
			'loading'       => 'eager',
		);
	}

	return array(
		'loading' => 'lazy',
	);
}

/**
 * Resolve the selected video thumbnail and its matching blur-up placeholder.
 *
 * @since 1.8.0
 *
 * @param int    $attachment_id Attachment ID.
 * @param string $thumbnail_url Optional pre-resolved thumbnail URL.
 * @return array<string, string>
 */
function rtgodam_get_video_thumbnail_sources( $attachment_id, $thumbnail_url = '' ) {
	$attachment_id = absint( $attachment_id );

	if ( ! $attachment_id ) {
		return array(
			'thumbnail'   => '',
			'placeholder' => '',
		);
	}

	$resolved_thumbnail   = '';
	$resolved_placeholder = '';

	if ( ! empty( $thumbnail_url ) && is_string( $thumbnail_url ) ) {
		$resolved_thumbnail = esc_url_raw( rtgodam_convert_to_https_url( $thumbnail_url ) );
	}

	if ( empty( $resolved_thumbnail ) ) {
		$custom_thumbnail = get_post_meta( $attachment_id, 'rtgodam_media_video_thumbnail', true );
		if ( ! empty( $custom_thumbnail ) && is_string( $custom_thumbnail ) ) {
			$resolved_thumbnail = esc_url_raw( rtgodam_convert_to_https_url( $custom_thumbnail ) );
		}
	}

	if ( empty( $resolved_thumbnail ) ) {
		$image = wp_get_attachment_image_url( $attachment_id, 'medium' );
		if ( $image ) {
			$resolved_thumbnail = esc_url_raw( $image );
		}
	}

	if ( ! empty( $resolved_thumbnail ) ) {
		$placeholder_map = get_post_meta( $attachment_id, 'rtgodam_media_placeholder_thumbnails', true );
		if ( is_array( $placeholder_map ) ) {
			$normalized_thumbnail = rtgodam_convert_to_https_url( $resolved_thumbnail );
			foreach ( $placeholder_map as $thumbnail_key => $placeholder_url ) {
				if ( empty( $thumbnail_key ) || empty( $placeholder_url ) ) {
					continue;
				}

				if ( rtgodam_convert_to_https_url( (string) $thumbnail_key ) === $normalized_thumbnail ) {
					$resolved_placeholder = esc_url_raw( rtgodam_convert_to_https_url( (string) $placeholder_url ) );
					break;
				}
			}
		}
	}

	if ( empty( $resolved_placeholder ) ) {
		$single_placeholder = get_post_meta( $attachment_id, 'rtgodam_media_video_placeholder_thumbnail', true );
		if ( ! empty( $single_placeholder ) && is_string( $single_placeholder ) ) {
			$resolved_placeholder = esc_url_raw( rtgodam_convert_to_https_url( $single_placeholder ) );
		}
	}

	if ( empty( $resolved_thumbnail ) ) {
		$icon = wp_mime_type_icon( $attachment_id );
		if ( $icon ) {
			$resolved_thumbnail = esc_url_raw( $icon );
		}
	}

	if ( empty( $resolved_thumbnail ) && defined( 'RTGODAM_URL' ) ) {
		$resolved_thumbnail = trailingslashit( RTGODAM_URL ) . 'assets/src/images/video-thumbnail-default.png';
	}

	return array(
		'thumbnail'   => $resolved_thumbnail,
		'placeholder' => $resolved_placeholder,
	);
}

/**
 * Format an associative array of HTML attributes into a string.
 *
 * @since 1.8.0
 *
 * @param array<string, scalar> $attributes Attributes to serialize.
 *
 * @return string
 */
function rtgodam_format_html_attributes( $attributes ) {
	if ( ! is_array( $attributes ) || empty( $attributes ) ) {
		return '';
	}

	$formatted = array();

	foreach ( $attributes as $name => $value ) {
		if ( ! is_scalar( $value ) || '' === $value ) {
			continue;
		}

		$name = strtolower( trim( (string) $name ) );

		if ( ! preg_match( '/^[a-z_:][-a-z0-9_:.]*$/', $name ) ) {
			continue;
		}

		$formatted[] = sprintf( '%s="%s"', $name, esc_attr( (string) $value ) );
	}

	return implode( ' ', $formatted );
}
