<?php
/**
 * MusicBrainz artwork lookup for VMPlayer.
 *
 * Two-step lookup: query MusicBrainz for a recording that matches
 * the artist and track title, then fetch cover art from the Cover
 * Art Archive for the release the recording belongs to.
 *
 * MusicBrainz publishes a rate limit of one request per second per IP.
 * Aggressive transient caching keeps us well under that limit even
 * for busy sites. Negative results are also cached (shorter TTL) so
 * we do not repeatedly ask for artwork that does not exist.
 *
 * @package VMPlayer
 */

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

/**
 * Class VMPlayer_MusicBrainz
 */
class VMPlayer_MusicBrainz {

    /**
     * MusicBrainz recording search endpoint.
     */
    const MB_SEARCH_URL = 'https://musicbrainz.org/ws/2/recording/';

    /**
     * Cover Art Archive URL template.
     */
    const CAA_URL_TEMPLATE = 'https://coverartarchive.org/release/%s/front-500';

    /**
     * Cache key prefix for successful artwork URLs.
     */
    const CACHE_PREFIX_HIT = 'vmplayer_mb_hit_';

    /**
     * Cache key prefix for negative results (no artwork found).
     */
    const CACHE_PREFIX_MISS = 'vmplayer_mb_miss_';

    /**
     * Positive result TTL. Cover art rarely changes.
     */
    const TTL_HIT = DAY_IN_SECONDS;

    /**
     * Negative result TTL. Shorter so newly added tracks can be found
     * without waiting a full day.
     */
    const TTL_MISS = 6 * HOUR_IN_SECONDS;

    /**
     * MusicBrainz requires an identifying User-Agent that includes
     * application name, version, and contact.
     */
    const USER_AGENT = 'VMPlayer/' . VMPLAYER_VERSION . ' (WordPress Plugin; https://gandenterprisesinc.com/vmplayer)';

    /**
     * HTTP timeout in seconds.
     */
    const TIMEOUT = 6;

    /**
     * Look up artwork for a track. Returns the front cover URL from
     * the Cover Art Archive, or empty string on any failure.
     *
     * @param string $artist Artist name.
     * @param string $title  Track title.
     * @return string Artwork URL or empty string.
     */
    public static function lookup_artwork( $artist, $title ) {
        $artist = trim( (string) $artist );
        $title  = trim( (string) $title );

        if ( '' === $artist || '' === $title ) {
            return '';
        }

        $cache_id = md5( strtolower( $artist ) . '|' . strtolower( $title ) );

        // Return cached hit if we have one.
        $hit = get_transient( self::CACHE_PREFIX_HIT . $cache_id );
        if ( false !== $hit && is_string( $hit ) && '' !== $hit ) {
            return $hit;
        }

        // Skip lookup entirely if we recently missed this pair.
        $miss = get_transient( self::CACHE_PREFIX_MISS . $cache_id );
        if ( false !== $miss ) {
            return '';
        }

        // Step 1: find a matching recording on MusicBrainz.
        $release_mbid = self::search_recording( $artist, $title );
        if ( '' === $release_mbid ) {
            set_transient( self::CACHE_PREFIX_MISS . $cache_id, 1, self::TTL_MISS );
            return '';
        }

        // Step 2: probe Cover Art Archive for a front cover on that release.
        $artwork_url = self::get_cover_art_url( $release_mbid );
        if ( '' === $artwork_url ) {
            set_transient( self::CACHE_PREFIX_MISS . $cache_id, 1, self::TTL_MISS );
            return '';
        }

        set_transient( self::CACHE_PREFIX_HIT . $cache_id, $artwork_url, self::TTL_HIT );
        return $artwork_url;
    }

    /* ------------------------------------------------------------------
     * Internal steps
     * --------------------------------------------------------------- */

    /**
     * Search MusicBrainz for a recording matching artist + title,
     * return the MBID of the release it belongs to.
     *
     * @param string $artist Artist name.
     * @param string $title  Track title.
     * @return string Release MBID or empty string.
     */
    private static function search_recording( $artist, $title ) {
        // Build a Lucene query using MusicBrainz's field syntax.
        // Quotes ensure phrase matching for multi-word values.
        $query = sprintf(
            'recording:"%s" AND artist:"%s"',
            self::escape_lucene( $title ),
            self::escape_lucene( $artist )
        );

        $url = add_query_arg(
            array(
                'query' => $query,
                'fmt'   => 'json',
                'limit' => 5,
            ),
            self::MB_SEARCH_URL
        );

        $response = wp_remote_get(
            $url,
            array(
                'timeout'     => self::TIMEOUT,
                'redirection' => 3,
                'sslverify'   => true,
                'user-agent'  => self::USER_AGENT,
                'headers'     => array(
                    'Accept' => 'application/json',
                ),
            )
        );

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

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

        if ( ! is_array( $data ) || empty( $data['recordings'] ) || ! is_array( $data['recordings'] ) ) {
            return '';
        }

        // Prefer the first recording that has a release with cover art
        // available. Recordings can appear on many releases; we pick
        // the first one for simplicity.
        foreach ( $data['recordings'] as $recording ) {
            if ( ! is_array( $recording ) || empty( $recording['releases'] ) ) {
                continue;
            }
            foreach ( $recording['releases'] as $release ) {
                if ( is_array( $release ) && ! empty( $release['id'] ) ) {
                    return (string) $release['id'];
                }
            }
        }

        return '';
    }

    /**
     * Ask Cover Art Archive whether a release has a front cover.
     * Returns the resolved archive.org replica URL when found so the
     * browser can hit it directly, skipping the roundtrip through
     * coverartarchive.org.
     *
     * @param string $release_mbid Release MBID from MusicBrainz.
     * @return string Cover art URL or empty string.
     */
    private static function get_cover_art_url( $release_mbid ) {
        $url = sprintf( self::CAA_URL_TEMPLATE, rawurlencode( $release_mbid ) );

        // Cover Art Archive answers front-cover requests with a 307
        // that redirects to whichever archive.org replica currently
        // holds the image (dn720704.ca.archive.org and similar). Our
        // original implementation returned the canonical CAA URL and
        // let the browser follow the redirect fresh on every load,
        // which was safe against replica hostname rotation but slow
        // whenever coverartarchive.org was under load. In practice
        // that meant listeners saw occasional stalls and broken
        // artwork slots even when the underlying replica was fine.
        //
        // We now do the redirect resolution once here and hand the
        // browser the resolved replica URL directly. That is fast in
        // the common case and self-healing in the rare case where a
        // replica rotates: our cache TTL expires, we re-resolve on
        // the next miss, and the frontend has an image error handler
        // that falls back to the station logo if the browser cannot
        // reach the replica in the meantime.
        $response = wp_remote_head(
            $url,
            array(
                'timeout'     => self::TIMEOUT,
                'redirection' => 0,
                'sslverify'   => true,
                'user-agent'  => self::USER_AGENT,
            )
        );

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

        $code = (int) wp_remote_retrieve_response_code( $response );

        // 200 is the direct-hit path. Rare for CAA but possible when
        // the release only has one canonical asset attached.
        if ( 200 === $code ) {
            return $url;
        }

        // 3xx family means art exists at the Location target. Extract
        // and return that URL so the browser bypasses coverartarchive.org
        // entirely on the actual image load. If Location is missing
        // for some reason (unusual), fall back to the canonical URL
        // so at least something has a chance of resolving.
        if ( in_array( $code, array( 301, 302, 307, 308 ), true ) ) {
            $location = wp_remote_retrieve_header( $response, 'location' );
            if ( is_string( $location ) && '' !== $location ) {
                return $location;
            }
            return $url;
        }

        // 404 means no cover art for this release.
        return '';
    }

    /**
     * Escape Lucene query syntax characters that appear in
     * artist/title values. MusicBrainz uses Lucene under the hood.
     *
     * @param string $value Value to escape.
     * @return string
     */
    private static function escape_lucene( $value ) {
        $chars = array( '\\', '+', '-', '&', '|', '!', '(', ')', '{', '}', '[', ']', '^', '"', '~', '*', '?', ':', '/' );
        return str_replace( $chars, array_map( function ( $c ) { return '\\' . $c; }, $chars ), $value );
    }
}
