<?php
/**
 * Admin functionality for VMPlayer.
 *
 * Registers the settings page under Settings menu, renders the three-tab
 * UI (General, Stations, About), and handles all AJAX endpoints for
 * saving settings, managing station presets, and testing stream URLs.
 *
 * All AJAX endpoints verify nonce and capability before processing.
 * All input is delegated to VMPlayer_Settings for sanitization.
 *
 * @package VMPlayer
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class VMPlayer_Admin
 */
class VMPlayer_Admin {

    /**
     * Plugin slug.
     *
     * @var string
     */
    private $plugin_slug;

    /**
     * Plugin version.
     *
     * @var string
     */
    private $version;

    /**
     * Admin page hook suffix returned by add_options_page.
     *
     * @var string
     */
    private $hook_suffix = '';

    /**
     * Nonce action name for all admin AJAX endpoints.
     */
    const NONCE_ACTION = 'vmplayer_admin_nonce';

    /**
     * Required capability for admin actions.
     */
    const REQUIRED_CAP = 'manage_options';

    /**
     * Constructor.
     *
     * @param string $plugin_slug Plugin slug.
     * @param string $version     Plugin version.
     */
    public function __construct( $plugin_slug, $version ) {
        $this->plugin_slug = $plugin_slug;
        $this->version     = $version;
    }

    /* ---------------------------------------------------------------------
     * Page registration and asset enqueuing
     * ------------------------------------------------------------------ */

    /**
     * Add the VMPlayer settings page under Settings.
     *
     * @return void
     */
    public function add_menu_page() {
        $this->hook_suffix = add_options_page(
            esc_html__( 'VMPlayer Settings', 'vmplayer' ),
            esc_html__( 'VMPlayer', 'vmplayer' ),
            self::REQUIRED_CAP,
            $this->plugin_slug,
            array( $this, 'render_settings_page' )
        );
    }

    /**
     * Enqueue admin CSS and JS only on the VMPlayer settings page.
     *
     * @param string $hook Current admin page hook.
     * @return void
     */
    public function enqueue_admin_assets( $hook ) {
        if ( $hook !== $this->hook_suffix ) {
            return;
        }

        // WordPress core dependencies.
        wp_enqueue_media();
        wp_enqueue_style( 'wp-color-picker' );

        // Plugin admin CSS. Uses filemtime-based versioning so the
        // browser always fetches the latest file whenever it changes
        // on disk, no plugin version bump required.
        wp_enqueue_style(
            'vmplayer-admin',
            VMPLAYER_PLUGIN_URL . 'assets/css/vmplayer-admin.css',
            array( 'wp-color-picker' ),
            $this->asset_version( 'assets/css/vmplayer-admin.css' )
        );

        // Plugin admin JS. Same filemtime versioning as the CSS above.
        wp_enqueue_script(
            'vmplayer-admin',
            VMPLAYER_PLUGIN_URL . 'assets/js/vmplayer-admin.js',
            array( 'jquery', 'wp-color-picker', 'jquery-ui-sortable' ),
            $this->asset_version( 'assets/js/vmplayer-admin.js' ),
            true
        );

        // Emit config as inline JSON attached to the admin script. We
        // deliberately avoid wp_localize_script here because that
        // helper runs every string value through htmlspecialchars,
        // which corrupts station names containing & or apostrophes
        // for downstream JS that reads the values into form inputs
        // and card titles. wp_json_encode preserves the original
        // characters and wp_add_inline_script is the WordPress-
        // idiomatic way to attach dynamic data to an enqueued handle
        // without hand rolling a script tag.
        wp_add_inline_script(
            'vmplayer-admin',
            'window.vmplayerAdminConfig = ' . wp_json_encode(
                array(
                    'ajaxUrl'         => admin_url( 'admin-ajax.php' ),
                    'nonce'           => wp_create_nonce( self::NONCE_ACTION ),
                    'settings'        => VMPlayer_Settings::get_settings(),
                    'stations'        => VMPlayer_Settings::get_stations(),
                    'stationLimit'    => VMPlayer_Settings::get_station_limit(),
                    'streamTypes'     => VMPlayer_Settings::get_supported_stream_types(),
                    'audioFormats'    => VMPlayer_Settings::get_supported_audio_formats(),
                    'socialPlatforms' => VMPlayer_Settings::get_supported_social_platforms(),
                    'socialConfig'    => VMPlayer_Settings::get_social_platform_config(),
                    'strings'         => $this->get_js_strings(),
                )
            ) . ';',
            'before'
        );
    }

    /**
     * Build a versioning string for a plugin-owned asset.
     * Uses the file's modification time appended to the plugin version
     * so any change to the file bumps the query string, forcing browsers
     * to fetch the fresh copy. Falls back to plugin version if the file
     * cannot be read.
     *
     * @param string $relative_path Path relative to plugin root, no leading slash.
     * @return string
     */
    private function asset_version( $relative_path ) {
        $full_path = VMPLAYER_PLUGIN_DIR . $relative_path;
        if ( file_exists( $full_path ) ) {
            return $this->version . '.' . filemtime( $full_path );
        }
        return $this->version;
    }

    /**
     * Translatable strings exposed to the admin JS module.
     *
     * @return array
     */
    private function get_js_strings() {
        return array(
            'saving'              => esc_html__( 'Saving...', 'vmplayer' ),
            'saved'               => esc_html__( 'Saved.', 'vmplayer' ),
            'saveFailed'          => esc_html__( 'Save failed. Please try again.', 'vmplayer' ),
            'deleteConfirm'       => esc_html__( 'Delete this station? This cannot be undone.', 'vmplayer' ),
            'deleting'            => esc_html__( 'Deleting...', 'vmplayer' ),
            'deleted'             => esc_html__( 'Station deleted.', 'vmplayer' ),
            'deleteFailed'        => esc_html__( 'Could not delete the station.', 'vmplayer' ),
            'testing'             => esc_html__( 'Testing connection...', 'vmplayer' ),
            'testSuccess'         => esc_html__( 'Stream is reachable.', 'vmplayer' ),
            'testFailure'         => esc_html__( 'Could not connect to stream.', 'vmplayer' ),
            'stationLimitReached' => esc_html__( 'You have reached the station preset limit.', 'vmplayer' ),
            'untitledStation'     => esc_html__( 'Untitled Station', 'vmplayer' ),
            'newMount'            => esc_html__( 'New Mount', 'vmplayer' ),
            'selectLogo'          => esc_html__( 'Select Station Logo', 'vmplayer' ),
            'useThisImage'        => esc_html__( 'Use This Image', 'vmplayer' ),
            'remove'              => esc_html__( 'Remove', 'vmplayer' ),
        );
    }

    /* ---------------------------------------------------------------------
     * Page rendering
     * ------------------------------------------------------------------ */

    /**
     * Render the settings page wrapper with tab navigation.
     *
     * @return void
     */
    public function render_settings_page() {
        if ( ! current_user_can( self::REQUIRED_CAP ) ) {
            wp_die( esc_html__( 'You do not have permission to access this page.', 'vmplayer' ) );
        }

        $active_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';
        $valid_tabs = array( 'general', 'stations', 'help', 'about' );
        if ( ! in_array( $active_tab, $valid_tabs, true ) ) {
            $active_tab = 'general';
        }

        ?>
        <div class="wrap vmplayer-admin-wrap">
            <h1><?php esc_html_e( 'VMPlayer Settings', 'vmplayer' ); ?></h1>

            <nav class="nav-tab-wrapper vmplayer-tabs" aria-label="<?php esc_attr_e( 'VMPlayer settings tabs', 'vmplayer' ); ?>">
                <a href="<?php echo esc_url( $this->get_tab_url( 'general' ) ); ?>"
                   class="nav-tab<?php echo 'general' === $active_tab ? ' nav-tab-active' : ''; ?>"
                   data-tab="general">
                    <?php esc_html_e( 'General', 'vmplayer' ); ?>
                </a>
                <a href="<?php echo esc_url( $this->get_tab_url( 'stations' ) ); ?>"
                   class="nav-tab<?php echo 'stations' === $active_tab ? ' nav-tab-active' : ''; ?>"
                   data-tab="stations">
                    <?php esc_html_e( 'Stations', 'vmplayer' ); ?>
                </a>
                <a href="<?php echo esc_url( $this->get_tab_url( 'help' ) ); ?>"
                   class="nav-tab<?php echo 'help' === $active_tab ? ' nav-tab-active' : ''; ?>"
                   data-tab="help">
                    <?php esc_html_e( 'Help', 'vmplayer' ); ?>
                </a>
                <a href="<?php echo esc_url( $this->get_tab_url( 'about' ) ); ?>"
                   class="nav-tab<?php echo 'about' === $active_tab ? ' nav-tab-active' : ''; ?>"
                   data-tab="about">
                    <?php esc_html_e( 'About', 'vmplayer' ); ?>
                </a>
            </nav>

            <div id="vmplayer-notice" class="vmplayer-notice" role="status" aria-live="polite"></div>

            <div class="vmplayer-tab-content">
                <?php
                switch ( $active_tab ) {
                    case 'stations':
                        $this->render_tab_stations();
                        break;
                    case 'help':
                        $this->render_tab_help();
                        break;
                    case 'about':
                        $this->render_tab_about();
                        break;
                    case 'general':
                    default:
                        $this->render_tab_general();
                        break;
                }
                ?>
            </div>
        </div>
        <?php
    }

    /**
     * Build the URL for a tab.
     *
     * @param string $tab Tab slug.
     * @return string
     */
    private function get_tab_url( $tab ) {
        return add_query_arg(
            array(
                'page' => $this->plugin_slug,
                'tab'  => $tab,
            ),
            admin_url( 'options-general.php' )
        );
    }

    /**
     * Render the General tab.
     *
     * @return void
     */
    private function render_tab_general() {
        $settings = VMPlayer_Settings::get_settings();
        $stations = VMPlayer_Settings::get_stations();
        ?>
        <div class="vmplayer-tab-panel" id="vmplayer-tab-general">
            <form id="vmplayer-general-form" class="vmplayer-form" novalidate>

                <table class="form-table" role="presentation">
                    <tbody>

                    <tr>
                        <th scope="row">
                            <label for="vmplayer-theme-primary"><?php esc_html_e( 'Primary Theme Color', 'vmplayer' ); ?></label>
                        </th>
                        <td>
                            <input type="text"
                                   id="vmplayer-theme-primary"
                                   name="theme_color_primary"
                                   value="<?php echo esc_attr( $settings['theme_color_primary'] ); ?>"
                                   class="vmplayer-color-picker"
                                   data-default-color="#a855f7">
                            <p class="description"><?php esc_html_e( 'Used for the play button gradient and accent highlights.', 'vmplayer' ); ?></p>
                        </td>
                    </tr>

                    <tr>
                        <th scope="row">
                            <label for="vmplayer-theme-secondary"><?php esc_html_e( 'Secondary Theme Color', 'vmplayer' ); ?></label>
                        </th>
                        <td>
                            <input type="text"
                                   id="vmplayer-theme-secondary"
                                   name="theme_color_secondary"
                                   value="<?php echo esc_attr( $settings['theme_color_secondary'] ); ?>"
                                   class="vmplayer-color-picker"
                                   data-default-color="#3b82f6">
                            <p class="description"><?php esc_html_e( 'Used for the volume slider, links, and visualizer accents.', 'vmplayer' ); ?></p>
                        </td>
                    </tr>

                    <tr>
                        <th scope="row"><?php esc_html_e( 'Player Features', 'vmplayer' ); ?></th>
                        <td>
                            <fieldset>
                                <legend class="screen-reader-text"><?php esc_html_e( 'Player Features', 'vmplayer' ); ?></legend>

                                <label for="vmplayer-enable-visualizer">
                                    <input type="checkbox"
                                           id="vmplayer-enable-visualizer"
                                           name="enable_visualizer"
                                           value="1"
                                           <?php checked( $settings['enable_visualizer'] ); ?>>
                                    <?php esc_html_e( 'Enable EQ visualizer', 'vmplayer' ); ?>
                                </label><br>

                                <label for="vmplayer-enable-popout">
                                    <input type="checkbox"
                                           id="vmplayer-enable-popout"
                                           name="enable_popout"
                                           value="1"
                                           <?php checked( $settings['enable_popout'] ); ?>>
                                    <?php esc_html_e( 'Enable pop-out player window', 'vmplayer' ); ?>
                                </label><br>

                                <label for="vmplayer-auto-resume">
                                    <input type="checkbox"
                                           id="vmplayer-auto-resume"
                                           name="auto_resume"
                                           value="1"
                                           <?php checked( $settings['auto_resume'] ); ?>>
                                    <?php esc_html_e( 'Auto-resume main player when pop-out closes', 'vmplayer' ); ?>
                                </label><br>

                                <label for="vmplayer-itunes">
                                    <input type="checkbox"
                                           id="vmplayer-itunes"
                                           name="itunes_enabled"
                                           value="1"
                                           <?php checked( $settings['itunes_enabled'] ); ?>>
                                    <?php esc_html_e( 'Use iTunes Search for track artwork', 'vmplayer' ); ?>
                                </label><br>

                                <label for="vmplayer-deezer">
                                    <input type="checkbox"
                                           id="vmplayer-deezer"
                                           name="deezer_enabled"
                                           value="1"
                                           <?php checked( $settings['deezer_enabled'] ); ?>>
                                    <?php esc_html_e( 'Use Deezer for track artwork (best coverage for international and Caribbean music)', 'vmplayer' ); ?>
                                </label><br>

                                <label for="vmplayer-musicbrainz">
                                    <input type="checkbox"
                                           id="vmplayer-musicbrainz"
                                           name="musicbrainz_enabled"
                                           value="1"
                                           <?php checked( $settings['musicbrainz_enabled'] ); ?>>
                                    <?php esc_html_e( 'Use MusicBrainz for track artwork', 'vmplayer' ); ?>
                                </label>
                            </fieldset>
                        </td>
                    </tr>

                    <tr>
                        <th scope="row">
                            <label for="vmplayer-itunes-country"><?php esc_html_e( 'iTunes Storefront', 'vmplayer' ); ?></label>
                        </th>
                        <td>
                            <select id="vmplayer-itunes-country" name="itunes_country">
                                <?php
                                $current_country = isset( $settings['itunes_country'] ) ? $settings['itunes_country'] : 'us';
                                foreach ( VMPlayer_Settings::get_supported_itunes_countries() as $code => $label ) :
                                    ?>
                                    <option value="<?php echo esc_attr( $code ); ?>" <?php selected( $current_country, $code ); ?>>
                                        <?php echo esc_html( $label ); ?>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                            <p class="description">
                                <?php esc_html_e( 'Which iTunes Store to search first. Pick the region your listeners live in so local artists get found faster. Deezer and MusicBrainz still run globally as fallbacks.', 'vmplayer' ); ?>
                            </p>
                        </td>
                    </tr>

                    <tr>
                        <th scope="row">
                            <label for="vmplayer-refresh-interval"><?php esc_html_e( 'Metadata Refresh Interval', 'vmplayer' ); ?></label>
                        </th>
                        <td>
                            <input type="number"
                                   id="vmplayer-refresh-interval"
                                   name="metadata_refresh_interval"
                                   value="<?php echo esc_attr( $settings['metadata_refresh_interval'] ); ?>"
                                   min="<?php echo esc_attr( VMPlayer_Settings::MIN_REFRESH_INTERVAL ); ?>"
                                   max="<?php echo esc_attr( VMPlayer_Settings::MAX_REFRESH_INTERVAL ); ?>"
                                   step="1"
                                   class="small-text">
                            <span><?php esc_html_e( 'seconds', 'vmplayer' ); ?></span>
                            <p class="description">
                                <?php
                                printf(
                                    /* translators: 1: minimum seconds, 2: maximum seconds */
                                    esc_html__( 'How often to fetch current track info. Range: %1$d to %2$d seconds.', 'vmplayer' ),
                                    (int) VMPlayer_Settings::MIN_REFRESH_INTERVAL,
                                    (int) VMPlayer_Settings::MAX_REFRESH_INTERVAL
                                );
                                ?>
                            </p>
                        </td>
                    </tr>

                    <tr>
                        <th scope="row">
                            <label for="vmplayer-default-station"><?php esc_html_e( 'Default Station', 'vmplayer' ); ?></label>
                        </th>
                        <td>
                            <?php if ( empty( $stations ) ) : ?>
                                <p>
                                    <em><?php esc_html_e( 'No stations configured yet. Add stations in the Stations tab.', 'vmplayer' ); ?></em>
                                </p>
                            <?php else : ?>
                                <select id="vmplayer-default-station" name="default_station">
                                    <?php foreach ( $stations as $index => $station ) : ?>
                                        <option value="<?php echo esc_attr( $index ); ?>"
                                                <?php selected( (int) $settings['default_station'], $index ); ?>>
                                            <?php echo esc_html( $station['name'] ); ?>
                                        </option>
                                    <?php endforeach; ?>
                                </select>
                                <p class="description"><?php esc_html_e( 'Which preset loads first when a visitor arrives.', 'vmplayer' ); ?></p>
                            <?php endif; ?>
                        </td>
                    </tr>

                    </tbody>
                </table>

                <p class="submit">
                    <button type="submit" class="button button-primary" id="vmplayer-save-general">
                        <?php esc_html_e( 'Save Changes', 'vmplayer' ); ?>
                    </button>
                </p>

            </form>
        </div>
        <?php
    }

    /**
     * Render the Stations tab.
     *
     * @return void
     */
    private function render_tab_stations() {
        $stations = VMPlayer_Settings::get_stations();
        $limit    = VMPlayer_Settings::get_station_limit();
        $count    = count( $stations );
        $can_add  = $count < $limit;
        ?>
        <div class="vmplayer-tab-panel" id="vmplayer-tab-stations">

            <div class="vmplayer-stations-header">
                <p class="vmplayer-stations-count">
                    <?php
                    printf(
                        /* translators: 1: current count, 2: limit */
                        esc_html__( '%1$d of %2$d station presets configured.', 'vmplayer' ),
                        (int) $count,
                        (int) $limit
                    );
                    ?>
                </p>
                <button type="button"
                        id="vmplayer-add-station"
                        class="button button-primary"
                        <?php disabled( ! $can_add ); ?>>
                    <?php esc_html_e( 'Add Station', 'vmplayer' ); ?>
                </button>
            </div>

            <div id="vmplayer-stations-list" class="vmplayer-stations-list">
                <?php if ( empty( $stations ) ) : ?>
                    <div class="vmplayer-empty-state">
                        <p>
                            <?php esc_html_e( 'No stations yet. Click Add Station to set up your first preset.', 'vmplayer' ); ?>
                        </p>
                    </div>
                <?php endif; ?>
            </div>

        </div>
        <?php
    }

    /**
     * Render the About tab.
     *
     * @return void
     */
    private function render_tab_about() {
        $settings = VMPlayer_Settings::get_settings();
        ?>
        <div class="vmplayer-tab-panel" id="vmplayer-tab-about">
            <div class="vmplayer-about-grid">

                <div class="vmplayer-about-card">
                    <h2><?php esc_html_e( 'About VMPlayer', 'vmplayer' ); ?></h2>
                    <p>
                        <?php esc_html_e( 'A modern streaming radio player for WordPress. Supports Shoutcast, Icecast, HLS, and direct streams with live metadata and a pop-out window.', 'vmplayer' ); ?>
                    </p>
                    <ul class="vmplayer-about-facts">
                        <li>
                            <strong><?php esc_html_e( 'Version:', 'vmplayer' ); ?></strong>
                            <?php echo esc_html( $this->version ); ?>
                        </li>
                        <li>
                            <strong><?php esc_html_e( 'Station limit:', 'vmplayer' ); ?></strong>
                            <?php echo esc_html( (string) VMPlayer_Settings::get_station_limit() ); ?>
                        </li>
                        <li>
                            <strong><?php esc_html_e( 'Stations configured:', 'vmplayer' ); ?></strong>
                            <?php echo esc_html( (string) VMPlayer_Settings::count_stations() ); ?>
                        </li>
                    </ul>
                </div>

                <div class="vmplayer-about-card">
                    <h2><?php esc_html_e( 'Add the Player', 'vmplayer' ); ?></h2>
                    <p>
                        <?php esc_html_e( 'Drop this shortcode into any post, page, block, or widget where you want the player to appear:', 'vmplayer' ); ?>
                    </p>
                    <p>
                        <code class="vmplayer-shortcode">[vmplayer]</code>
                    </p>
                    <p class="description">
                        <?php esc_html_e( 'To force a specific station, add a station attribute:', 'vmplayer' ); ?>
                        <br>
                        <code class="vmplayer-shortcode">[vmplayer station="1"]</code>
                        <?php esc_html_e( 'or', 'vmplayer' ); ?>
                        <code class="vmplayer-shortcode">[vmplayer station="my-slug"]</code>
                    </p>
                </div>

                <div class="vmplayer-about-card vmplayer-about-premium">
                    <h2><?php esc_html_e( 'Premium Coming Soon', 'vmplayer' ); ?></h2>
                    <p>
                        <?php esc_html_e( 'A premium version will lift the 5-station limit and add custom visualizer styles, listener counts, schedule display, and more.', 'vmplayer' ); ?>
                    </p>
                </div>

                <div class="vmplayer-about-card vmplayer-about-credits">
                    <h2><?php esc_html_e( 'Credits', 'vmplayer' ); ?></h2>
                    <a href="https://gandenterprisesinc.com"
                       target="_blank"
                       rel="noopener noreferrer"
                       class="vmplayer-credits-logo-link"
                       aria-label="<?php esc_attr_e( 'G and D Enterprises Inc. website', 'vmplayer' ); ?>">
                        <img src="https://i.ibb.co/Q7Dmv9Fc/logo.png"
                             alt="<?php esc_attr_e( 'G and D Enterprises Inc.', 'vmplayer' ); ?>"
                             class="vmplayer-credits-logo" />
                    </a>
                    <p class="vmplayer-credits-line">
                        <?php esc_html_e( 'Built by G &amp; D Enterprises Inc.', 'vmplayer' ); ?>
                    </p>
                    <p class="vmplayer-credits-description">
                        <?php esc_html_e( 'G &amp; D Enterprises Inc. is a professional multi-service company dedicated to helping you succeed. We provide highly efficient, reliable solutions for a wide range of needs.', 'vmplayer' ); ?>
                    </p>
                </div>

                <?php if ( empty( $settings['is_pro'] ) ) : ?>
                <div class="vmplayer-about-card vmplayer-about-sponsor">
                    <p class="vmplayer-sponsor-label">
                        <?php esc_html_e( 'Sponsor', 'vmplayer' ); ?>
                    </p>
                    <a href="https://vouscast.com" target="_blank" rel="noopener noreferrer sponsored" class="vmplayer-sponsor-link">
                        <img src="https://i.ibb.co/d47mffpw/vousbanner.jpg"
                             alt="<?php esc_attr_e( 'Vouscast Media Platform', 'vmplayer' ); ?>"
                             class="vmplayer-sponsor-banner" />
                    </a>
                    <p class="vmplayer-sponsor-caption">
                        <?php esc_html_e( 'VMPlayer is named for the Vouscast Media Platform. Professional streaming hosting for radio stations.', 'vmplayer' ); ?>
                    </p>
                </div>
                <?php endif; ?>

            </div>
        </div>
        <?php
    }

    /**
     * Render the Help tab. A static reference page for admins covering
     * setup, common troubleshooting, and FAQs. Content is intentionally
     * long-form so an admin who lands here in a moment of frustration
     * can find their answer without leaving the WordPress dashboard.
     *
     * @return void
     */
    private function render_tab_help() {
        ?>
        <div class="vmplayer-tab-panel" id="vmplayer-tab-help">
            <div class="vmplayer-help-wrap">

                <div class="vmplayer-help-intro">
                    <h2><?php esc_html_e( 'Getting Started with VMPlayer', 'vmplayer' ); ?></h2>
                    <p>
                        <?php esc_html_e( 'Quick answers to the questions most admins have when setting up VMPlayer for the first time or troubleshooting an issue. If you get stuck on anything not covered here, contact support at gandenterprisesinc.com.', 'vmplayer' ); ?>
                    </p>
                </div>

                <details class="vmplayer-help-section" open>
                    <summary><h3><?php esc_html_e( 'Setup Walkthrough', 'vmplayer' ); ?></h3></summary>
                    <div class="vmplayer-help-body">
                        <ol>
                            <li>
                                <strong><?php esc_html_e( 'Add a station.', 'vmplayer' ); ?></strong>
                                <?php esc_html_e( 'Open the Stations tab and click Add Station. Give it a name, drop in your stream URL, pick the stream type (Shoutcast, Icecast, HLS, or direct), and upload a station logo. The logo shows in the artwork slot until per-song artwork is found.', 'vmplayer' ); ?>
                            </li>
                            <li>
                                <strong><?php esc_html_e( 'Test the connection.', 'vmplayer' ); ?></strong>
                                <?php esc_html_e( 'Use the Test Connection button on the station card. A green result means VMPlayer can reach your stream. A red result usually means the stream URL is wrong, the server is down, or CORS headers are blocking access.', 'vmplayer' ); ?>
                            </li>
                            <li>
                                <strong><?php esc_html_e( 'Set your iTunes storefront.', 'vmplayer' ); ?></strong>
                                <?php esc_html_e( 'On the General tab, pick the country whose iTunes Store best matches your listeners. This helps VMPlayer find local artists faster during artwork lookups.', 'vmplayer' ); ?>
                            </li>
                            <li>
                                <strong><?php esc_html_e( 'Drop the shortcode into a page.', 'vmplayer' ); ?></strong>
                                <?php esc_html_e( 'Use [vmplayer] in any post, page, widget, or block. To force a specific station on a specific page, use [vmplayer station="1"] where 1 is the index, or [vmplayer station="my-station-slug"] for a named station.', 'vmplayer' ); ?>
                            </li>
                            <li>
                                <strong><?php esc_html_e( 'Give the player room.', 'vmplayer' ); ?></strong>
                                <?php esc_html_e( 'The player renders best when its container is at least 1200 pixels wide. Below that it collapses gracefully to a compact layout and eventually to a portrait layout for mobile. If you drop it inside a narrow theme content column, it will still work but will render in portrait mode.', 'vmplayer' ); ?>
                            </li>
                        </ol>
                    </div>
                </details>

                <details class="vmplayer-help-section">
                    <summary><h3><?php esc_html_e( 'Frequently Asked Questions', 'vmplayer' ); ?></h3></summary>
                    <div class="vmplayer-help-body">
                        <h4><?php esc_html_e( 'What stream types are supported?', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'Shoutcast v1 and v2, Icecast (all versions), HLS (.m3u8 playlists), and direct MP3, AAC, OGG, or Opus streams. If your host runs SonicPanel, Zeno, or Vouscast, those all sit on top of Shoutcast or Icecast, so pick the underlying protocol when configuring the station.', 'vmplayer' ); ?>
                        </p>

                        <h4><?php esc_html_e( 'How does VMPlayer find song artwork?', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'It checks four sources in order. First, the stream itself, in case your automation software pushes an artwork URL through custom metadata fields. Then iTunes Search API, which has strong indie coverage through aggregators. Then Deezer, which has particularly strong reggae, Caribbean, and international catalog. Then MusicBrainz for older and classical releases. If none of the four has a match, the station logo shows instead.', 'vmplayer' ); ?>
                        </p>

                        <h4><?php esc_html_e( 'How many stations can I add?', 'vmplayer' ); ?></h4>
                        <p>
                            <?php
                            printf(
                                /* translators: %d: station limit */
                                esc_html__( 'The free version allows up to %d stations. Listeners can cycle between them using the previous and next buttons on the player.', 'vmplayer' ),
                                (int) VMPlayer_Settings::get_station_limit()
                            );
                            ?>
                        </p>

                        <h4><?php esc_html_e( 'Does VMPlayer work on mobile?', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'Yes. The player uses CSS container queries so it responds to its own container width, not the viewport. On narrow containers it switches to a portrait layout that fits comfortably on phone screens.', 'vmplayer' ); ?>
                        </p>

                        <h4><?php esc_html_e( 'Does the pop-out window work on all browsers?', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'The pop-out uses standard window.open() which works in every modern browser. Some listeners have pop-up blockers that will require them to allow pop-ups from your site the first time they click Pop Out. This is a browser policy, not something the plugin controls.', 'vmplayer' ); ?>
                        </p>

                        <h4><?php esc_html_e( 'Where is the visualizer getting its data?', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'The visualizer runs a decorative CSS animation by default. Real-time audio-reactive analysis requires the stream to send CORS headers, which most streaming hosts do not. Turning on real-time analysis on a non-CORS stream can silence the audio entirely because of a Web Audio API security rule, which is why we run in decorative mode by default.', 'vmplayer' ); ?>
                        </p>

                        <h4><?php esc_html_e( 'Can listeners switch between stations without reloading?', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'Yes. If more than one station is configured, the previous and next buttons cycle between them. The current station indicator shows which one is playing.', 'vmplayer' ); ?>
                        </p>
                    </div>
                </details>

                <details class="vmplayer-help-section">
                    <summary><h3><?php esc_html_e( 'Troubleshooting', 'vmplayer' ); ?></h3></summary>
                    <div class="vmplayer-help-body">
                        <h4><?php esc_html_e( 'No audio plays when I hit Play.', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'First, click Test Connection on the Stations tab to confirm VMPlayer can reach the stream. If the test passes but audio still fails on the frontend, open your browser DevTools console and click Play. If you see a NotAllowedError, the browser blocked autoplay and the listener will need to click the play button after page load. If you see a NetworkError or a CORS error, the stream URL is likely wrong or the server is unreachable.', 'vmplayer' ); ?>
                        </p>

                        <h4><?php esc_html_e( 'Artwork shows a broken image icon.', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'VMPlayer automatically falls back to the station logo when an artwork URL fails to load. If you are still seeing broken images, make sure your station has a logo uploaded on the Stations tab. If a specific song consistently shows the station logo, none of the artwork sources have that recording in their catalog.', 'vmplayer' ); ?>
                        </p>

                        <h4><?php esc_html_e( 'Metadata is not updating.', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'VMPlayer polls for new metadata every 15 seconds by default. You can change that on the General tab if you need faster or slower updates. Very short intervals can trigger rate limits at your streaming host, so 10 to 30 seconds is the sweet spot.', 'vmplayer' ); ?>
                        </p>

                        <h4><?php esc_html_e( 'The player renders tall and narrow on a wide screen.', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'The player responds to its container width, not the viewport. If your theme or page builder puts the shortcode inside a narrow column, the player collapses to portrait mode. Either widen the section holding the shortcode, or set that section to full width in your page builder.', 'vmplayer' ); ?>
                        </p>

                        <h4><?php esc_html_e( 'Changes to settings do not show up on the frontend.', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'The plugin includes cache-busting on its own assets, but if your site uses a caching plugin or a CDN like Cloudflare, the browser may still be holding stale files. Try a hard refresh (Ctrl+Shift+R on Windows, Cmd+Shift+R on Mac). If you have Cloudflare, purge the cache from the Cloudflare dashboard.', 'vmplayer' ); ?>
                        </p>

                        <h4><?php esc_html_e( 'Test Connection returns red even though the stream plays elsewhere.', 'vmplayer' ); ?></h4>
                        <p>
                            <?php esc_html_e( 'The most common cause is a stream URL that only serves audio through a specific path, or a server that requires HTTPS. Check that your URL matches exactly what your streaming host lists on their control panel, including the mount point (like /stream or /listen) and the correct port.', 'vmplayer' ); ?>
                        </p>
                    </div>
                </details>

                <details class="vmplayer-help-section">
                    <summary><h3><?php esc_html_e( 'Shortcode Reference', 'vmplayer' ); ?></h3></summary>
                    <div class="vmplayer-help-body">
                        <p>
                            <code>[vmplayer]</code>
                            <?php esc_html_e( 'Renders the player with all configured stations, defaulting to the one marked as default on the General tab.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <code>[vmplayer station="2"]</code>
                            <?php esc_html_e( 'Renders the player starting on the third station (indexes start at 0).', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <code>[vmplayer station="my-station-slug"]</code>
                            <?php esc_html_e( 'Renders the player starting on a station matched by its slug.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <code>[vmplayer variant="compact"]</code>
                            <?php esc_html_e( 'Renders the smaller two-row landscape variant, sized for sidebars, footer strips, and other placements where the main player is too large. Combine with the station attribute to feature a specific station in a compact placement.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <code>[vmplayer_compact]</code>
                            <?php esc_html_e( 'Alias for the compact variant. Bypasses attribute parsing entirely and works reliably in page builders that would otherwise mangle straight quotes. Also accepts an optional station attribute.', 'vmplayer' ); ?>
                        </p>
                    </div>
                </details>

                <details class="vmplayer-help-section">
                    <summary><h3><?php esc_html_e( 'LIVE Broadcast Detection', 'vmplayer' ); ?></h3></summary>
                    <div class="vmplayer-help-body">
                        <p>
                            <?php esc_html_e( 'When a DJ or host goes on air using a physical console, a broadcast software like BUTT, or any encoder that does not send song metadata to the stream server, the player would otherwise keep showing the last automation song title from before the live source connected. The Detect Live Broadcasts option on each station card fixes that by displaying LIVE instead of the leftover title.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <strong><?php esc_html_e( 'How detection works:', 'vmplayer' ); ?></strong>
                        </p>
                        <ul>
                            <li><?php esc_html_e( 'Empty stream title (typical when using BUTT or a console without a metadata bridge).', 'vmplayer' ); ?></li>
                            <li><?php esc_html_e( 'Stream title matches the station name (some encoders default to this).', 'vmplayer' ); ?></li>
                            <li><?php esc_html_e( 'Stream title matches one of the live indicator keywords you configure per station. Real songs almost always have "Artist - Title" format, so the keyword check ignores titles that contain a hyphen surrounded by spaces to avoid false positives on song titles that happen to contain the word "live".', 'vmplayer' ); ?></li>
                            <li><?php esc_html_e( 'Stale metadata detection. When the same song title has been reported for longer than a typical song (about 8 minutes by default), the source is treated as live. This catches the console and BUTT scenario where the encoder never updates the title after the last automation song ended, so the server keeps reporting the same track long after it stopped playing.', 'vmplayer' ); ?></li>
                        </ul>
                        <p>
                            <strong><?php esc_html_e( 'Configuring live indicators:', 'vmplayer' ); ?></strong>
                            <?php esc_html_e( 'The default list covers common phrases (live, on air, live show, live mix, live dj, live stream). Add your own show names, DJ handles, or any other markers your automation software uses to indicate a live source. Separate multiple indicators with commas.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <strong><?php esc_html_e( 'When to leave it off:', 'vmplayer' ); ?></strong>
                            <?php esc_html_e( 'If your automation software (RadioBoss, StationPlaylist, RadioDJ) always sends accurate song metadata even during live shows, you probably do not need this. Detection is opt-in per station because a station operator knows their own workflow best.', 'vmplayer' ); ?>
                        </p>
                    </div>
                </details>

                <details class="vmplayer-help-section">
                    <summary><h3><?php esc_html_e( 'Listener Count Display', 'vmplayer' ); ?></h3></summary>
                    <div class="vmplayer-help-body">
                        <p>
                            <?php esc_html_e( 'The Show Listener Count option on each station card enables a small chip on the player displaying the current number of active listeners. It is per-station because a station operator might want the number visible for busy stations to build social proof and hidden for quieter ones.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <strong><?php esc_html_e( 'Server support:', 'vmplayer' ); ?></strong>
                        </p>
                        <ul>
                            <li><?php esc_html_e( 'Shoutcast v1: read from the /7.html endpoint.', 'vmplayer' ); ?></li>
                            <li><?php esc_html_e( 'Shoutcast v2: read from the CURRENTLISTENERS field in the stats XML.', 'vmplayer' ); ?></li>
                            <li><?php esc_html_e( 'Icecast: read from the listeners field in status-json.xsl.', 'vmplayer' ); ?></li>
                            <li><?php esc_html_e( 'HLS and direct streams: no listener data available. The chip stays hidden even when the option is on.', 'vmplayer' ); ?></li>
                        </ul>
                        <p>
                            <strong><?php esc_html_e( 'What if the count does not appear?', 'vmplayer' ); ?></strong>
                            <?php esc_html_e( 'Some hosting providers strip listener data from the stats endpoint. The chip hides automatically when the server does not report a count, so nothing broken shows up on the player. Test Connection on the station card reports what the server returned so you can verify.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <strong><?php esc_html_e( 'What if turning the option off does not hide the count?', 'vmplayer' ); ?></strong>
                            <?php esc_html_e( 'This is almost always a page caching issue. If a WordPress caching plugin (WP Rocket, W3 Total Cache, LiteSpeed Cache, or your host caching layer) is serving cached HTML, the old markup keeps showing the chip until the cache expires or is purged. Purge the cache for the page that renders the player, then hard-refresh the browser (Ctrl+Shift+R or Cmd+Shift+R) to make sure you are not looking at a stale copy. The same applies to the LIVE detection and any other station display option.', 'vmplayer' ); ?>
                        </p>
                    </div>
                </details>

                <details class="vmplayer-help-section">
                    <summary><h3><?php esc_html_e( 'Clock Display', 'vmplayer' ); ?></h3></summary>
                    <div class="vmplayer-help-body">
                        <p>
                            <?php esc_html_e( 'The Show Clock option on each station card controls whether the current time and date appear in the top row of the main player and pop-out. Defaults to on so existing stations keep the clock they had before this option existed. Turn it off per station if you want a cleaner header or if the placement is too narrow for the clock to fit gracefully.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <strong><?php esc_html_e( 'Compact variant note:', 'vmplayer' ); ?></strong>
                            <?php esc_html_e( 'The compact two-row layout does not include a clock area at all, regardless of this setting. The two-row format leaves no room for it, and listeners looking at a compact player usually have their own device clock visible.', 'vmplayer' ); ?>
                        </p>
                    </div>
                </details>

                <details class="vmplayer-help-section">
                    <summary><h3><?php esc_html_e( 'Compact Variant with Page Builders', 'vmplayer' ); ?></h3></summary>
                    <div class="vmplayer-help-body">
                        <p>
                            <?php esc_html_e( 'Some page builders (Elementor, WPBakery, and others that texturize content) convert straight quotes in shortcodes into curly typographic quotes before rendering. That transformation breaks WordPress\'s shortcode attribute parser, and the variant="compact" attribute silently drops. On those sites the compact shortcode either renders nothing or falls back to the main variant.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <strong><?php esc_html_e( 'Reliable syntax for page builders:', 'vmplayer' ); ?></strong>
                            <?php esc_html_e( 'Use the dedicated compact shortcode instead of the attribute form. Both produce the same output; the dedicated form has no attributes to mangle.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <code>[vmplayer_compact]</code>
                            <?php esc_html_e( 'Renders the compact variant. Works everywhere the regular vmplayer shortcode does.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <code>[vmplayer_compact station="my-station-slug"]</code>
                            <?php esc_html_e( 'Same as above but starts on a specific station. Note that even the station attribute here uses straight quotes; page builders that mangle those quotes may still misroute this. If your page builder is aggressive about quote conversion, use no attributes and let the plugin start on your default station.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <?php esc_html_e( 'The attribute form [vmplayer variant="compact"] also still works, and 1.1.4 adds curly-quote tolerance so even a builder that swaps the quotes should now render correctly. The dedicated shortcode is a belt-and-braces fallback for the most aggressive builders.', 'vmplayer' ); ?>
                        </p>
                    </div>
                </details>

                <details class="vmplayer-help-section">
                    <summary><h3><?php esc_html_e( 'Auto-Resume After Page Reload', 'vmplayer' ); ?></h3></summary>
                    <div class="vmplayer-help-body">
                        <p>
                            <?php esc_html_e( 'Once a listener presses play, the player resumes automatically after any page reload in the same tab session. If the page refreshes on its own, if the visitor navigates and hits the back button, or if the tab crashes and reopens, playback picks up where it left off with no click needed. Closing the tab clears the flag so a fresh visit starts paused.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <strong><?php esc_html_e( 'Browser autoplay policy:', 'vmplayer' ); ?></strong>
                            <?php esc_html_e( 'Auto-resume only works after the listener has clicked play at least once during their visit. Browsers block audio autoplay on first visit as a matter of policy, so a listener who has not yet interacted with the player will still see the paused state after any refresh. This is not something the plugin controls; it is a browser rule that applies to all audio.', 'vmplayer' ); ?>
                        </p>
                        <p>
                            <strong><?php esc_html_e( 'When does auto-resume stop?', 'vmplayer' ); ?></strong>
                            <?php esc_html_e( 'Explicit user actions clear the intent flag: pressing pause on the player, closing the tab entirely, or opening the site in a new tab. Any of these means the next page load starts paused. Simple refreshes, back/forward navigation, and tab restore preserve the intent flag and auto-resume playback.', 'vmplayer' ); ?>
                        </p>
                    </div>
                </details>

                <div class="vmplayer-help-footer">
                    <p>
                        <?php esc_html_e( 'Still stuck? Reach out to G and D Enterprises at', 'vmplayer' ); ?>
                        <a href="https://gandenterprisesinc.com" target="_blank" rel="noopener noreferrer">gandenterprisesinc.com</a>.
                    </p>
                </div>

            </div>
        </div>
        <?php
    }

    /* ---------------------------------------------------------------------
     * AJAX endpoints
     * ------------------------------------------------------------------ */

    /**
     * Verify nonce and capability for an AJAX request.
     * Sends a JSON error and exits on failure.
     *
     * @return void
     */
    private function verify_ajax_request() {
        if ( ! check_ajax_referer( self::NONCE_ACTION, 'nonce', false ) ) {
            wp_send_json_error(
                array( 'message' => esc_html__( 'Security check failed. Please reload the page.', 'vmplayer' ) ),
                403
            );
        }

        if ( ! current_user_can( self::REQUIRED_CAP ) ) {
            wp_send_json_error(
                array( 'message' => esc_html__( 'You do not have permission to perform this action.', 'vmplayer' ) ),
                403
            );
        }
    }

    /**
     * Save global settings.
     *
     * @return void
     */
    public function ajax_save_settings() {
        $this->verify_ajax_request();

        $raw = isset( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : array();
        if ( ! is_array( $raw ) ) {
            wp_send_json_error(
                array( 'message' => esc_html__( 'Invalid settings payload.', 'vmplayer' ) ),
                400
            );
        }

        // Coerce checkbox-style values. Unchecked checkboxes are absent.
        $bool_keys = array(
            'enable_visualizer',
            'enable_popout',
            'auto_resume',
            'itunes_enabled',
            'deezer_enabled',
            'musicbrainz_enabled',
        );
        foreach ( $bool_keys as $key ) {
            $raw[ $key ] = ! empty( $raw[ $key ] );
        }

        $result = VMPlayer_Settings::update_settings( $raw );
        if ( ! $result ) {
            // update_option returns false when the value is unchanged.
            // That is not an error from the user's perspective.
        }

        wp_send_json_success(
            array(
                'message'  => esc_html__( 'Settings saved.', 'vmplayer' ),
                'settings' => VMPlayer_Settings::get_settings(),
            )
        );
    }

    /**
     * Add a new station or update an existing one.
     *
     * @return void
     */
    public function ajax_save_station() {
        $this->verify_ajax_request();

        $index_raw = isset( $_POST['index'] ) ? wp_unslash( $_POST['index'] ) : '';
        $station   = isset( $_POST['station'] ) ? wp_unslash( $_POST['station'] ) : array();

        if ( ! is_array( $station ) ) {
            wp_send_json_error(
                array( 'message' => esc_html__( 'Invalid station payload.', 'vmplayer' ) ),
                400
            );
        }

        // Empty index means add new; numeric index means update.
        if ( '' === $index_raw || null === $index_raw ) {
            $result = VMPlayer_Settings::add_station( $station );
            if ( is_wp_error( $result ) ) {
                wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
            }
            wp_send_json_success(
                array(
                    'message'  => esc_html__( 'Station added.', 'vmplayer' ),
                    'index'    => (int) $result,
                    'station'  => VMPlayer_Settings::get_station( (int) $result ),
                    'stations' => VMPlayer_Settings::get_stations(),
                )
            );
        }

        $index  = absint( $index_raw );
        $result = VMPlayer_Settings::update_station( $index, $station );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
        }

        wp_send_json_success(
            array(
                'message'  => esc_html__( 'Station saved.', 'vmplayer' ),
                'index'    => $index,
                'station'  => VMPlayer_Settings::get_station( $index ),
                'stations' => VMPlayer_Settings::get_stations(),
            )
        );
    }

    /**
     * Delete a station preset.
     *
     * @return void
     */
    public function ajax_delete_station() {
        $this->verify_ajax_request();

        $index = isset( $_POST['index'] ) ? absint( wp_unslash( $_POST['index'] ) ) : -1;
        if ( $index < 0 ) {
            wp_send_json_error(
                array( 'message' => esc_html__( 'Invalid station index.', 'vmplayer' ) ),
                400
            );
        }

        $result = VMPlayer_Settings::delete_station( $index );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
        }

        wp_send_json_success(
            array(
                'message'  => esc_html__( 'Station deleted.', 'vmplayer' ),
                'stations' => VMPlayer_Settings::get_stations(),
                'settings' => VMPlayer_Settings::get_settings(),
            )
        );
    }

    /**
     * Reorder stations.
     *
     * @return void
     */
    public function ajax_reorder_stations() {
        $this->verify_ajax_request();

        $order = isset( $_POST['order'] ) ? wp_unslash( $_POST['order'] ) : array();
        if ( ! is_array( $order ) ) {
            wp_send_json_error(
                array( 'message' => esc_html__( 'Invalid order payload.', 'vmplayer' ) ),
                400
            );
        }

        // Cast every index to int.
        $order = array_map( 'absint', $order );

        $result = VMPlayer_Settings::reorder_stations( $order );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
        }

        wp_send_json_success(
            array(
                'message'  => esc_html__( 'Stations reordered.', 'vmplayer' ),
                'stations' => VMPlayer_Settings::get_stations(),
            )
        );
    }

    /**
     * Test a stream URL for reachability.
     *
     * Two-phase probe. Phase 1 sends HEAD, which is fast when the
     * server supports it. Phase 2 falls back to GET with a Range header
     * for servers behind reverse proxies (Cloudflare, nginx-fronted
     * streams, path-routed setups) that reject HEAD but happily serve
     * range requests. We look at both the HTTP status and the ICY
     * headers (icy-name, icy-br) that Shoutcast and Icecast use to
     * announce themselves, so a status other than 200 still counts
     * as a stream if the ICY signature is present.
     *
     * @return void
     */
    public function ajax_test_stream() {
        $this->verify_ajax_request();

        $url  = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
        $type = isset( $_POST['stream_type'] ) ? sanitize_key( wp_unslash( $_POST['stream_type'] ) ) : '';

        if ( empty( $url ) ) {
            wp_send_json_error(
                array( 'message' => esc_html__( 'Stream URL is required.', 'vmplayer' ) ),
                400
            );
        }

        $start = microtime( true );

        // User-Agent that reverse proxies do not filter out.
        $ua = 'Mozilla/5.0 (compatible; VMPlayer/' . $this->version . ')';

        // Phase 1: HEAD. Fast path for well-behaved servers.
        $response = wp_remote_head(
            $url,
            array(
                'timeout'     => 3,
                'redirection' => 3,
                'sslverify'   => false,
                'user-agent'  => $ua,
                'headers'     => array(
                    'Icy-MetaData' => '1',
                ),
            )
        );

        $head_failed = is_wp_error( $response );
        $head_code   = $head_failed ? 0 : (int) wp_remote_retrieve_response_code( $response );

        // Phase 2 conditions: HEAD errored, or returned a hard failure code.
        // 405 (Method Not Allowed) means the server IS there but does not
        // like HEAD, so we fall back to GET too. 200-399 from HEAD is
        // good enough on its own.
        $needs_get_fallback = $head_failed || $head_code >= 400;

        // Track values populated by whichever phase supplies them.
        $probe_bytes = 0;
        $probe_error = '';

        if ( $needs_get_fallback ) {
            // Direct cURL probe so we can abort the read after a small
            // sample. wp_remote_get's limit_response_size does not
            // reliably stop chunked audio streams on services like
            // Zeno, RadioJar, MyRadioStream, AsuraHosting proxy, and
            // RadioKing, all of which ignore Range headers and just
            // start pouring audio at us. Without the abort, the test
            // hangs for the full timeout even though the stream is
            // definitely reachable, which used to flag working stations
            // as "cURL error 28: Operation timed out". We now treat
            // "timed out but received bytes" as reachable, since a
            // server actively streaming audio to us is by definition
            // reachable.
            $probe = $this->probe_stream_with_curl( $url, $ua, 5 );

            if ( $probe['reachable'] ) {
                // Synthesize a WP-style response array so downstream
                // code that expects wp_remote_get shape keeps working
                // and picks up the values we already extracted from
                // the cURL headers.
                $probe_bytes = $probe['bytes_received'];
                $response    = array(
                    'response' => array( 'code' => $probe['status_code'] ),
                    'headers'  => array(
                        'content-type' => $probe['content_type'],
                        'icy-name'     => $probe['icy_name'],
                        'icy-br'       => $probe['icy_bitrate'],
                    ),
                    'body'     => '',
                );
            } else {
                // cURL probe failed too. If cURL is not available on
                // this host, fall back to the legacy wp_remote_get
                // path so we still probe something. Otherwise report
                // the specific cURL error the probe surfaced.
                if ( ! function_exists( 'curl_init' ) ) {
                    $response = wp_remote_get(
                        $url,
                        array(
                            'timeout'             => 5,
                            'redirection'         => 3,
                            'sslverify'           => false,
                            'user-agent'          => $ua,
                            'limit_response_size' => 2048,
                            'headers'             => array(
                                'Range'        => 'bytes=0-1',
                                'Icy-MetaData' => '1',
                                'Accept'       => 'audio/*,application/ogg,application/vnd.apple.mpegurl,*/*;q=0.5',
                            ),
                        )
                    );
                } else {
                    $probe_error = $probe['error'];
                    $response    = new WP_Error(
                        'vmplayer_probe_failed',
                        '' !== $probe_error ? $probe_error : esc_html__( 'Stream did not respond in time.', 'vmplayer' )
                    );
                }
            }
        }

        $elapsed_ms = (int) round( ( microtime( true ) - $start ) * 1000 );
        $method     = $needs_get_fallback ? 'GET' : 'HEAD';

        // Transport-level failure (DNS, connection refused, TLS, timeout).
        if ( is_wp_error( $response ) ) {
            wp_send_json_success(
                array(
                    'reachable'    => false,
                    'status_code'  => 0,
                    'content_type' => '',
                    'icy_name'     => '',
                    'icy_bitrate'  => '',
                    'is_audio'     => false,
                    'method'       => $method,
                    'elapsed_ms'   => $elapsed_ms,
                    'stream_type'  => $type,
                    'message'      => sprintf(
                        /* translators: %s: error message from the HTTP client */
                        esc_html__( 'Could not connect: %s', 'vmplayer' ),
                        $response->get_error_message()
                    ),
                )
            );
        }

        $code         = (int) wp_remote_retrieve_response_code( $response );
        $content_type = (string) wp_remote_retrieve_header( $response, 'content-type' );
        $icy_name     = (string) wp_remote_retrieve_header( $response, 'icy-name' );
        $icy_br       = (string) wp_remote_retrieve_header( $response, 'icy-br' );

        $is_audio     = $this->looks_like_audio_stream( $content_type, $icy_name, $icy_br );

        // Reachable if we got any non-server-error HTTP status code back,
        // OR if the response has an audio signature regardless of status.
        // Some servers return 200, some 206 (partial from Range), some
        // even 400 with ICY headers attached. The signature wins.
        $is_reachable = $is_audio || ( $code > 0 && $code < 500 );

        // When reachable, do a protocol-aware metadata fetch so the admin
        // sees the actual station name and current track (if the server
        // exposes them) instead of just "reachable". Skip for unreachable
        // streams to avoid extra HTTP calls on obvious failures.
        $probe_metadata = array();
        if ( $is_reachable && class_exists( 'VMPlayer_Metadata' ) ) {
            $probe          = VMPlayer_Metadata::probe_stream( $url, $type );
            $probe_metadata = isset( $probe['metadata'] ) && is_array( $probe['metadata'] )
                ? $probe['metadata']
                : array();

            // Fold the metadata station name into icy_name so the JS
            // display picks it up either way.
            if ( '' === $icy_name && ! empty( $probe_metadata['station_name'] ) ) {
                $icy_name = (string) $probe_metadata['station_name'];
            }
            if ( '' === $icy_br && ! empty( $probe_metadata['bitrate'] ) ) {
                $icy_br = (string) $probe_metadata['bitrate'];
            }
        }

        wp_send_json_success(
            array(
                'reachable'    => $is_reachable,
                'status_code'  => $code,
                'content_type' => $content_type,
                'icy_name'     => $icy_name,
                'icy_bitrate'  => $icy_br,
                'is_audio'     => $is_audio,
                'method'       => $method,
                'elapsed_ms'   => $elapsed_ms,
                'stream_type'  => $type,
                'metadata'     => $probe_metadata,
                'message'      => $this->format_probe_message( $is_reachable, $is_audio, $code, $icy_name, $probe_metadata ),
            )
        );
    }

    /**
     * Low-level probe using direct cURL with a write callback so we
     * can abort the read cleanly after a small sample. WP HTTP API's
     * limit_response_size does not reliably stop chunked audio
     * responses, which is why streaming services like Zeno, RadioJar,
     * MyRadioStream, AsuraHosting proxy, and RadioKing all used to
     * time out this method with "cURL error 28" even though the
     * streams themselves work perfectly in the frontend player. This
     * probe gives us the same reachability signal without waiting for
     * a full timeout on servers that never bother respecting Range,
     * and it treats "timeout with bytes received" as a success
     * signal since a server actively streaming audio to us is by
     * definition reachable.
     *
     * @param string $url     Stream URL to probe.
     * @param string $ua      User-Agent header value.
     * @param int    $timeout Overall transfer timeout in seconds.
     * @return array {
     *     Result of the probe.
     *
     *     @type bool   $reachable      True when we got any response.
     *     @type int    $status_code    HTTP status code or 0.
     *     @type string $content_type   Content-Type header value.
     *     @type string $icy_name       icy-name header value.
     *     @type string $icy_bitrate    icy-br header value.
     *     @type int    $bytes_received Body bytes read before abort.
     *     @type string $error          Error text when unreachable.
     * }
     */
    private function probe_stream_with_curl( $url, $ua, $timeout = 5 ) {
        $result = array(
            'reachable'      => false,
            'status_code'    => 0,
            'content_type'   => '',
            'icy_name'       => '',
            'icy_bitrate'    => '',
            'bytes_received' => 0,
            'error'          => '',
        );

        if ( ! function_exists( 'curl_init' ) ) {
            $result['error'] = 'cURL not available';
            return $result;
        }

        $ch = curl_init( $url );
        if ( false === $ch ) {
            $result['error'] = 'Could not initialize cURL';
            return $result;
        }

        $headers_raw    = array();
        $bytes_received = 0;
        $max_body_bytes = 4096;

        curl_setopt_array(
            $ch,
            array(
                CURLOPT_CONNECTTIMEOUT => 3,
                CURLOPT_TIMEOUT        => (int) $timeout,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_MAXREDIRS      => 3,
                CURLOPT_SSL_VERIFYPEER => false,
                CURLOPT_SSL_VERIFYHOST => 0,
                CURLOPT_USERAGENT      => $ua,
                CURLOPT_HTTPHEADER     => array(
                    'Accept: audio/*,application/ogg,application/vnd.apple.mpegurl,*/*;q=0.5',
                    'Icy-MetaData: 1',
                    'Range: bytes=0-4095',
                ),
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_NOBODY         => false,
                CURLOPT_HEADERFUNCTION => function ( $handle, $header_line ) use ( &$headers_raw ) {
                    $headers_raw[] = trim( $header_line );
                    return strlen( $header_line );
                },
                CURLOPT_WRITEFUNCTION  => function ( $handle, $chunk ) use ( &$bytes_received, $max_body_bytes ) {
                    $bytes_received += strlen( $chunk );
                    if ( $bytes_received >= $max_body_bytes ) {
                        // Returning a byte count different from the
                        // chunk length signals cURL to abort. cURL
                        // sets errno 23 (CURLE_WRITE_ERROR) which
                        // we treat as an intentional stop below.
                        return 0;
                    }
                    return strlen( $chunk );
                },
            )
        );

        // Suppress error output; we inspect curl_errno directly.
        @curl_exec( $ch );

        $errno     = curl_errno( $ch );
        $err_msg   = curl_error( $ch );
        $http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
        curl_close( $ch );

        $result['bytes_received'] = $bytes_received;
        $result['status_code']    = $http_code;

        // Parse captured headers for the values downstream code needs.
        foreach ( $headers_raw as $header ) {
            if ( '' === $header ) {
                continue;
            }
            if ( stripos( $header, 'content-type:' ) === 0 ) {
                $result['content_type'] = trim( substr( $header, strlen( 'content-type:' ) ) );
            } elseif ( stripos( $header, 'icy-name:' ) === 0 ) {
                $result['icy_name'] = trim( substr( $header, strlen( 'icy-name:' ) ) );
            } elseif ( stripos( $header, 'icy-br:' ) === 0 ) {
                $result['icy_bitrate'] = trim( substr( $header, strlen( 'icy-br:' ) ) );
            }
        }

        $has_icy     = ( '' !== $result['icy_name'] ) || ( '' !== $result['icy_bitrate'] );
        $has_data    = $bytes_received > 0;
        $good_status = $http_code >= 200 && $http_code < 400;

        // Success ladder:
        //   errno 0  = clean exit
        //   errno 23 = our write callback aborted the transfer after
        //              collecting a sample, which means the stream
        //              was actively serving us bytes
        //   errno 28 = timeout, but if we received bytes the server
        //              is streaming to us and the stream is reachable
        //   ICY headers or a 2xx/3xx status code are strong signals
        //              even in edge cases
        if ( 0 === $errno ) {
            $result['reachable'] = $good_status || $has_data || $has_icy;
            return $result;
        }
        if ( 23 === $errno && ( $has_data || $has_icy ) ) {
            $result['reachable'] = true;
            return $result;
        }
        if ( 28 === $errno && ( $has_data || $has_icy ) ) {
            $result['reachable'] = true;
            return $result;
        }
        if ( $has_icy ) {
            $result['reachable'] = true;
            return $result;
        }

        $result['error'] = '' !== $err_msg ? $err_msg : ( 'cURL error ' . $errno );
        return $result;
    }

    /**
     * Decide whether a response looks like an audio stream.
     * Any ICY header is a strong signal. Audio content-types
     * are the secondary signal.
     *
     * @param string $content_type Content-Type header value.
     * @param string $icy_name     icy-name header value.
     * @param string $icy_br       icy-br header value.
     * @return bool
     */
    private function looks_like_audio_stream( $content_type, $icy_name, $icy_br ) {
        if ( '' !== $icy_name || '' !== $icy_br ) {
            return true;
        }

        $ct = strtolower( $content_type );
        $audio_prefixes = array(
            'audio/',
            'application/ogg',
            'application/vnd.apple.mpegurl',
            'application/x-mpegurl',
        );
        foreach ( $audio_prefixes as $prefix ) {
            if ( false !== strpos( $ct, $prefix ) ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Format a user-facing message describing the probe result.
     *
     * @param bool   $reachable Whether the endpoint responded.
     * @param bool   $is_audio  Whether audio signature was detected.
     * @param int    $code      HTTP status code.
     * @param string $icy_name  Station name from icy-name header.
     * @param array  $metadata  Optional metadata from probe_stream().
     * @return string
     */
    private function format_probe_message( $reachable, $is_audio, $code, $icy_name, $metadata = array() ) {
        // Preferred display: station name plus current track if the
        // metadata handler managed to fetch both.
        $station = $icy_name;
        if ( '' === $station && ! empty( $metadata['station_name'] ) ) {
            $station = (string) $metadata['station_name'];
        }

        $current_track = '';
        if ( is_array( $metadata ) && ! empty( $metadata['title'] ) ) {
            $artist = ! empty( $metadata['artist'] ) ? (string) $metadata['artist'] : '';
            $title  = (string) $metadata['title'];
            $current_track = '' !== $artist ? $artist . ' - ' . $title : $title;
        }

        if ( $is_audio || '' !== $station || '' !== $current_track ) {
            if ( '' !== $station && '' !== $current_track ) {
                return sprintf(
                    /* translators: 1: station name, 2: current track */
                    esc_html__( 'Stream reachable. Station: %1$s. Now playing: %2$s', 'vmplayer' ),
                    $station,
                    $current_track
                );
            }
            if ( '' !== $station ) {
                return sprintf(
                    /* translators: %s: station name */
                    esc_html__( 'Stream is reachable. Detected: %s', 'vmplayer' ),
                    $station
                );
            }
            if ( '' !== $current_track ) {
                return sprintf(
                    /* translators: %s: current track */
                    esc_html__( 'Stream is reachable. Now playing: %s', 'vmplayer' ),
                    $current_track
                );
            }
            return esc_html__( 'Stream is reachable. Audio content detected.', 'vmplayer' );
        }

        if ( $reachable ) {
            return sprintf(
                /* translators: %d: HTTP status code */
                esc_html__( 'Server responded with status %d, but no audio stream signature was detected.', 'vmplayer' ),
                $code
            );
        }

        return sprintf(
            /* translators: %d: HTTP status code */
            esc_html__( 'Server returned error %d.', 'vmplayer' ),
            $code
        );
    }
}
