<?php
/**
 * iTunes Search API artwork lookup for VMPlayer.
 *
 * The iTunes Search API is a free public endpoint with no authentication
 * or key required. It covers Apple Music's full catalog, which includes
 * a very wide range of indie releases pulled in through aggregators like
 * DistroKid, TuneCore, CD Baby, and Symphonic. That makes it a strong
 * fallback source for artists that never made it into MusicBrainz.
 *
 * Apple documents the rate limit as approximately 20 calls per minute
 * per IP. Aggressive transient caching keeps every site well under
 * that ceiling. We cache both hits and misses so we do not repeatedly
 * ask iTunes for songs that are not in their catalog.
 *
 * Country matters. The Search API's storefront defaults to the US, but
 * many indie and regional artists are only listed in their local
 * storefront. The admin can select the country that matches their
 * audience so those local artists get found on the first attempt.
 *
 * Apple's terms allow this "promo content" (album art) to be shown
 * when it promotes the underlying content. A now-playing display on a
 * streaming radio player is exactly that use case: listeners see what
 * is playing and can look it up to buy or stream.
 *
 * @package VMPlayer
 */

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

/**
 * Class VMPlayer_iTunes
 */
class VMPlayer_iTunes {

    /**
     * iTunes Search endpoint. Documented at
     * https://performance-partners.apple.com/search-api
     */
    const SEARCH_URL = 'https://itunes.apple.com/search';

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

    /**
     * Cache key prefix for negative results.
     */
    const CACHE_PREFIX_MISS = 'vmp_itunes_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 a long wait.
     */
    const TTL_MISS = 6 * HOUR_IN_SECONDS;

    /**
     * HTTP timeout in seconds. Kept short so a slow response does not
     * hold up a metadata poll.
     */
    const TIMEOUT = 6;

    /**
     * Preferred artwork size in pixels. iTunes serves images at any
     * multiple of 100 by replacing the size segment in the URL.
     */
    const PREFERRED_SIZE = 600;

    /**
     * User-Agent for identification.
     */
    const USER_AGENT = 'VMPlayer/' . VMPLAYER_VERSION . ' (WordPress Plugin; https://gandenterprisesinc.com/vmplayer)';

    /**
     * Look up artwork for a track. Returns a canonical iTunes CDN URL
     * on success, or an empty string when no reasonable match is found.
     *
     * @param string $artist  Artist name.
     * @param string $title   Track title.
     * @param string $country ISO country code for the iTunes storefront.
     * @return string Artwork URL or empty string.
     */
    public static function lookup_artwork( $artist, $title, $country = 'us' ) {
        $artist  = trim( (string) $artist );
        $title   = trim( (string) $title );
        $country = strtolower( trim( (string) $country ) );

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

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

        // Positive cache first. Hit means we already know the URL.
        $cached = get_transient( self::CACHE_PREFIX_HIT . $cache_id );
        if ( ! empty( $cached ) ) {
            return (string) $cached;
        }
        // Negative cache. Miss means we already asked and found nothing.
        $miss = get_transient( self::CACHE_PREFIX_MISS . $cache_id );
        if ( '1' === (string) $miss ) {
            return '';
        }

        $artwork_url = self::search( $artist, $title, $country );

        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;
    }

    /**
     * Perform the actual HTTP search. Handles URL building, request,
     * JSON decoding, match selection, and size upgrade.
     *
     * @param string $artist  Artist name.
     * @param string $title   Track title.
     * @param string $country Storefront code.
     * @return string Best artwork URL or empty string.
     */
    private static function search( $artist, $title, $country ) {
        $url = add_query_arg(
            array(
                'term'    => $artist . ' ' . $title,
                'media'   => 'music',
                'entity'  => 'song',
                'country' => $country,
                'limit'   => 10,
            ),
            self::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 ) ) {
            return '';
        }
        if ( 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['results'] ) || ! is_array( $data['results'] ) ) {
            return '';
        }

        $best = self::pick_best_match( $data['results'], $artist, $title );
        if ( empty( $best ) || empty( $best['artworkUrl100'] ) ) {
            return '';
        }

        return self::upgrade_size( (string) $best['artworkUrl100'], self::PREFERRED_SIZE );
    }

    /**
     * Pick the result that most closely matches the requested artist
     * and title. iTunes returns results in relevance order, but the
     * top result is not always the right one, especially for cover
     * versions and tribute albums. This walks the list and prefers
     * exact artist matches over partial ones.
     *
     * @param array  $results  Decoded results array from iTunes.
     * @param string $artist   Requested artist.
     * @param string $title    Requested title.
     * @return array Best match, or empty array if none look right.
     */
    private static function pick_best_match( $results, $artist, $title ) {
        $target_artist = self::normalize_for_compare( $artist );
        $target_title  = self::normalize_for_compare( $title );

        $exact  = array();
        $strong = array();
        $weak   = array();

        foreach ( $results as $result ) {
            if ( ! is_array( $result ) ) {
                continue;
            }
            $r_artist = self::normalize_for_compare( isset( $result['artistName'] ) ? $result['artistName'] : '' );
            $r_title  = self::normalize_for_compare( isset( $result['trackName'] ) ? $result['trackName'] : '' );

            if ( '' === $r_artist || '' === $r_title ) {
                continue;
            }

            $artist_hit = ( $r_artist === $target_artist );
            $title_hit  = ( $r_title === $target_title );
            $artist_soft = $artist_hit || false !== strpos( $r_artist, $target_artist ) || false !== strpos( $target_artist, $r_artist );
            $title_soft  = $title_hit  || false !== strpos( $r_title, $target_title )   || false !== strpos( $target_title, $r_title );

            if ( $artist_hit && $title_hit ) {
                $exact[] = $result;
            } elseif ( $artist_hit && $title_soft ) {
                $strong[] = $result;
            } elseif ( $artist_soft && $title_soft ) {
                $weak[] = $result;
            }
        }

        if ( ! empty( $exact ) ) {
            return $exact[0];
        }
        if ( ! empty( $strong ) ) {
            return $strong[0];
        }
        if ( ! empty( $weak ) ) {
            return $weak[0];
        }
        return array();
    }

    /**
     * Normalize a string for comparison. Lowercased, whitespace
     * collapsed, punctuation stripped. Handles common variations
     * like curly quotes, extra spaces, and featured-artist notations.
     *
     * @param string $value Raw value.
     * @return string
     */
    private static function normalize_for_compare( $value ) {
        $value = strtolower( trim( (string) $value ) );
        // Strip featured-artist notations that would prevent a match.
        $value = preg_replace( '/\s*\(feat\.?[^)]+\)/i', '', $value );
        $value = preg_replace( '/\s*feat\.?\s+.*$/i', '', $value );
        // Collapse punctuation and whitespace.
        $value = preg_replace( '/[^\p{L}\p{N}]+/u', ' ', $value );
        $value = preg_replace( '/\s+/', ' ', $value );
        return trim( (string) $value );
    }

    /**
     * Upgrade the iTunes artwork URL to a larger size. The default
     * URLs are 100x100. iTunes serves any square size by replacing
     * the "100x100" segment in the filename with the desired size.
     *
     * @param string $url  Original URL, typically ending in 100x100bb.jpg.
     * @param int    $size Target size in pixels.
     * @return string
     */
    private static function upgrade_size( $url, $size ) {
        $size = (int) $size;
        if ( $size < 100 ) {
            $size = 100;
        }
        // Only touch the size segment. The suffix (bb, -75, etc.) is
        // preserved so we do not break Apple's URL scheme. iTunes uses
        // two URL formats in the wild: the older one puts the size
        // after a dot in the filename ('mzi.xxxx.100x100-75.jpg'),
        // the newer thumb format puts it after a slash
        // ('.../source/100x100bb.jpg'). Matching either shape.
        return preg_replace( '/(\d+)x(\d+)([-a-z0-9]*)\.(jpe?g|png)$/i', $size . 'x' . $size . '$3.$4', $url );
    }
}
