<?php
/**
 * Shoutcast metadata handler.
 *
 * Tries Shoutcast stats endpoints in order of specificity, then
 * falls back to the ICY headers that Shoutcast serves directly
 * on the audio URL. Handles both Shoutcast v1 (with /7.html) and
 * v2 (with /stats and /currentsong endpoints under a sid).
 *
 * Works with reverse-proxied setups (path-routed URLs like
 * https://radio.example.com/8014/stream) because it derives the
 * base URL from the stream URL and appends stats paths to it.
 *
 * @package VMPlayer
 */

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

/**
 * Class VMPlayer_Shoutcast
 */
class VMPlayer_Shoutcast {

    /**
     * User-Agent for stats requests.
     */
    const USER_AGENT = 'Mozilla/5.0 (compatible; VMPlayer/' . VMPLAYER_VERSION . ')';

    /**
     * HTTP timeout for stats endpoints in seconds.
     */
    const TIMEOUT = 5;

    /**
     * Fetch Shoutcast metadata for a stream URL.
     * Tries multiple endpoints in order and returns the first
     * successful result. Falls back to ICY headers as a last resort.
     *
     * @param string $stream_url The stream URL.
     * @return array Partial metadata array. Empty on total failure.
     */
    public static function fetch_metadata( $stream_url ) {
        $base = VMPlayer_Metadata::derive_base_url( $stream_url );
        if ( '' === $base ) {
            return VMPlayer_Metadata::fetch_icy_headers( $stream_url );
        }

        // Try v1 /7.html first. Fast, simple, widely supported.
        $meta = self::fetch_7html( $base );
        if ( ! empty( $meta['title'] ) || ! empty( $meta['station_name'] ) ) {
            $meta['source'] = 'shoutcast-v1';
            return $meta;
        }

        // Try v2 /stats?sid=1 XML.
        $meta = self::fetch_stats_xml( $base );
        if ( ! empty( $meta['title'] ) || ! empty( $meta['station_name'] ) ) {
            $meta['source'] = 'shoutcast-v2';
            return $meta;
        }

        // Try v2 /currentsong?sid=1 for just the song text.
        $current = self::fetch_currentsong( $base );
        if ( '' !== $current ) {
            list( $artist, $title ) = VMPlayer_Metadata::parse_artist_title( $current );
            return array(
                'artist' => $artist,
                'title'  => $title,
                'source' => 'shoutcast-currentsong',
            );
        }

        // Universal fallback: ICY headers from the stream itself.
        $icy = VMPlayer_Metadata::fetch_icy_headers( $stream_url );
        if ( ! empty( $icy ) ) {
            $icy['source'] = 'icy-headers';
        }
        return $icy;
    }

    /* ------------------------------------------------------------------
     * Endpoint fetchers
     * --------------------------------------------------------------- */

    /**
     * Fetch and parse the Shoutcast v1 /7.html endpoint.
     * The response body is a single HTML line with seven comma
     * separated fields. The seventh field is the current song.
     * Example body: <HTML><BODY>4,1,42,32,25,128,Artist - Title</BODY></HTML>
     *
     * @param string $base_url Base URL with trailing slash.
     * @return array Partial metadata or empty array.
     */
    private static function fetch_7html( $base_url ) {
        $response = wp_remote_get(
            $base_url . '7.html',
            array(
                'timeout'     => self::TIMEOUT,
                'redirection' => 3,
                'sslverify'   => false,
                'user-agent'  => self::USER_AGENT,
            )
        );

        if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
            return array();
        }

        $body = wp_remote_retrieve_body( $response );
        if ( '' === $body ) {
            return array();
        }

        // Extract the body content between <BODY> tags.
        if ( ! preg_match( '/<body[^>]*>(.*?)<\/body>/is', $body, $m ) ) {
            return array();
        }

        // The last field can contain commas (song titles often do),
        // so cap the split at 7 fields to keep the song intact.
        $parts = explode( ',', trim( $m[1] ), 7 );
        if ( count( $parts ) < 7 ) {
            return array();
        }

        $song                    = trim( $parts[6] );
        list( $artist, $title )  = VMPlayer_Metadata::parse_artist_title( $song );

        // 7.html field order is documented as:
        //   [0]=currentlisteners, [1]=streamstatus, [2]=peaklisteners,
        //   [3]=maxlisteners, [4]=uniquelisteners, [5]=bitrate,
        //   [6]=songtitle
        // We surface currentlisteners as listener_count, streamstatus
        // as stream_online (source connected vs. silence), and reserve
        // is_live for the live-broadcast detection flag set later in
        // the metadata layer.
        return array(
            'title'          => $title,
            'artist'         => $artist,
            'bitrate'        => (int) $parts[5],
            'listener_count' => (int) $parts[0],
            'stream_online'  => 1 === (int) $parts[1],
        );
    }

    /**
     * Fetch and parse the Shoutcast v2 /stats?sid=1 XML endpoint.
     * Returns detailed server info including SONGTITLE, BITRATE,
     * SERVERGENRE, STREAMSTATUS.
     *
     * @param string $base_url Base URL with trailing slash.
     * @return array Partial metadata or empty array.
     */
    private static function fetch_stats_xml( $base_url ) {
        $response = wp_remote_get(
            $base_url . 'stats?sid=1',
            array(
                'timeout'     => self::TIMEOUT,
                'redirection' => 3,
                'sslverify'   => false,
                'user-agent'  => self::USER_AGENT,
            )
        );

        if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
            return array();
        }

        $body = wp_remote_retrieve_body( $response );
        if ( '' === $body ) {
            return array();
        }

        // simplexml is bundled with virtually all PHP builds but a
        // handful of stripped-down hosts disable it. Bail cleanly if
        // absent so the metadata pipeline moves on to the next fallback.
        if ( ! function_exists( 'simplexml_load_string' ) ) {
            return array();
        }

        // Load XML with error suppression. Malformed XML returns false.
        $prev_errors = libxml_use_internal_errors( true );
        $xml         = simplexml_load_string( $body );
        libxml_clear_errors();
        libxml_use_internal_errors( $prev_errors );

        if ( false === $xml ) {
            return array();
        }

        $result = array();

        if ( isset( $xml->SONGTITLE ) ) {
            $song                   = trim( (string) $xml->SONGTITLE );
            list( $artist, $title ) = VMPlayer_Metadata::parse_artist_title( $song );
            $result['title']        = $title;
            $result['artist']       = $artist;
        }
        if ( isset( $xml->SERVERTITLE ) ) {
            $result['station_name'] = trim( (string) $xml->SERVERTITLE );
        }
        if ( isset( $xml->SERVERGENRE ) ) {
            $result['genre'] = trim( (string) $xml->SERVERGENRE );
        }
        if ( isset( $xml->BITRATE ) ) {
            $result['bitrate'] = (int) $xml->BITRATE;
        }
        // Shoutcast v2 stats sometimes includes these two, though the
        // tag name varies across builds. Handle both spellings.
        if ( isset( $xml->SAMPLERATE ) ) {
            $result['sample_rate'] = (int) $xml->SAMPLERATE;
        } elseif ( isset( $xml->SAMPLE_RATE ) ) {
            $result['sample_rate'] = (int) $xml->SAMPLE_RATE;
        }
        if ( isset( $xml->CHANNELS ) ) {
            $result['channels'] = (int) $xml->CHANNELS;
        }
        // Listener count. Shoutcast v2 stats report CURRENTLISTENERS
        // as the count of clients actively connected. A missing tag
        // means the server did not expose it; treat as no data rather
        // than as zero listeners so the frontend can hide the display
        // instead of showing a misleading zero.
        if ( isset( $xml->CURRENTLISTENERS ) ) {
            $result['listener_count'] = (int) $xml->CURRENTLISTENERS;
        }
        // Shoutcast STREAMSTATUS just tells us the server currently
        // has a source connected. It does not distinguish between an
        // automation playlist and a human DJ. We track this separately
        // as stream_online so the live-broadcast detection flag can
        // stay reserved for the title-keyword check that actually
        // identifies a live source.
        if ( isset( $xml->STREAMSTATUS ) ) {
            $result['stream_online'] = 1 === (int) $xml->STREAMSTATUS;
        }
        if ( isset( $xml->CONTENT ) ) {
            $result['content_type'] = trim( (string) $xml->CONTENT );
        }

        // Shoutcast v2 stats can include per-song artwork under one of
        // a handful of tag names, depending on server version and how
        // the source client populates it. Flatten every scalar child
        // to lowercase keys and hand it to the shared extractor. If
        // one matches, prefer it over external artwork lookups.
        $flat = array();
        foreach ( $xml->children() as $node ) {
            $name = strtolower( $node->getName() );
            $val  = trim( (string) $node );
            if ( '' !== $val ) {
                $flat[ $name ] = $val;
            }
        }
        $artwork = VMPlayer_Metadata::extract_stream_artwork( $flat );
        if ( '' !== $artwork ) {
            $result['artwork_url']    = $artwork;
            $result['artwork_source'] = 'stream';
        }

        return $result;
    }

    /**
     * Fetch just the current song string from v2 /currentsong?sid=1.
     * Some servers expose this even when /stats is disabled.
     *
     * @param string $base_url Base URL with trailing slash.
     * @return string The raw song string, or empty on failure.
     */
    private static function fetch_currentsong( $base_url ) {
        $response = wp_remote_get(
            $base_url . 'currentsong?sid=1',
            array(
                'timeout'     => 3,
                'redirection' => 3,
                'sslverify'   => false,
                'user-agent'  => self::USER_AGENT,
            )
        );

        if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
            return '';
        }

        return trim( (string) wp_remote_retrieve_body( $response ) );
    }
}
