<?php
/**
 * Deezer Search API artwork lookup for VMPlayer.
 *
 * Deezer runs a free public search endpoint that does not require an
 * API key or authentication for basic catalog lookups. Their reggae,
 * Caribbean, Latin, African, and general non-Western catalog is
 * noticeably stronger than iTunes or MusicBrainz, which makes Deezer a
 * good complement for stations that play international music the
 * bigger databases undercount.
 *
 * The published rate limit is 50 requests per 5 seconds, well above
 * anything a metadata poller could reasonably hit. We still cache
 * hits and misses aggressively so a station broadcasting the same song
 * for three minutes does not send a lookup on every metadata poll.
 *
 * Deezer's terms explicitly forbid storing the artwork image itself,
 * but they permit hotlinking the CDN URL. This class caches URLs only,
 * and the browser fetches images directly from Deezer's CDN, so the
 * usage stays compliant.
 *
 * @package VMPlayer
 */

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

/**
 * Class VMPlayer_Deezer
 */
class VMPlayer_Deezer {

    /**
     * Deezer track search endpoint. Documented at
     * https://developers.deezer.com/api/search
     */
    const SEARCH_URL = 'https://api.deezer.com/search/track';

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

    /**
     * Cache key prefix for negative results.
     */
    const CACHE_PREFIX_MISS = 'vmp_deezer_miss_';

    /**
     * Positive result TTL. Album covers rarely change once released.
     */
    const TTL_HIT = DAY_IN_SECONDS;

    /**
     * Negative result TTL. Shorter so brand new releases can be found
     * before a full day has passed.
     */
    const TTL_MISS = 6 * HOUR_IN_SECONDS;

    /**
     * HTTP timeout in seconds. Deezer usually responds under 300ms.
     */
    const TIMEOUT = 6;

    /**
     * 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 Deezer CDN URL
     * on success, or an empty string when Deezer has no match.
     *
     * @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 ) );

        $cached = get_transient( self::CACHE_PREFIX_HIT . $cache_id );
        if ( ! empty( $cached ) ) {
            return (string) $cached;
        }
        $miss = get_transient( self::CACHE_PREFIX_MISS . $cache_id );
        if ( '1' === (string) $miss ) {
            return '';
        }

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

        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. Deezer supports an advanced
     * query syntax that lets us ask for artist and track as separate
     * fields instead of one blended search term. That reduces false
     * positives compared to a general full-text search.
     *
     * @param string $artist Artist name.
     * @param string $title  Track title.
     * @return string Best artwork URL or empty string.
     */
    private static function search( $artist, $title ) {
        $q = sprintf(
            'artist:"%s" track:"%s"',
            self::escape_query_value( $artist ),
            self::escape_query_value( $title )
        );

        $url = add_query_arg(
            array(
                'q'     => $q,
                '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['data'] ) || ! is_array( $data['data'] ) ) {
            return '';
        }

        return self::pick_best_artwork( $data['data'], $artist, $title );
    }

    /**
     * Walk the result list looking for the best album cover URL.
     * Deezer's response ranks by relevance so the first result is
     * usually right, but we still verify the artist and title match
     * loosely before accepting it. That avoids showing the wrong
     * album cover when Deezer returns a similarly-named track from
     * a different artist as its top hit.
     *
     * @param array  $results Deezer results array.
     * @param string $artist  Requested artist.
     * @param string $title   Requested title.
     * @return string Cover URL or empty string.
     */
    private static function pick_best_artwork( $results, $artist, $title ) {
        $target_artist = self::normalize_for_compare( $artist );
        $target_title  = self::normalize_for_compare( $title );

        foreach ( $results as $result ) {
            if ( ! is_array( $result ) || empty( $result['album'] ) || ! is_array( $result['album'] ) ) {
                continue;
            }

            $r_artist_name = isset( $result['artist']['name'] ) ? (string) $result['artist']['name'] : '';
            $r_title       = isset( $result['title'] ) ? (string) $result['title'] : '';

            $r_artist = self::normalize_for_compare( $r_artist_name );
            $r_title_norm = self::normalize_for_compare( $r_title );

            $artist_ok = ( $r_artist === $target_artist )
                || ( '' !== $target_artist && false !== strpos( $r_artist, $target_artist ) )
                || ( '' !== $r_artist && false !== strpos( $target_artist, $r_artist ) );

            $title_ok = ( $r_title_norm === $target_title )
                || ( '' !== $target_title && false !== strpos( $r_title_norm, $target_title ) )
                || ( '' !== $r_title_norm && false !== strpos( $target_title, $r_title_norm ) );

            if ( ! $artist_ok || ! $title_ok ) {
                continue;
            }

            // Prefer the biggest available cover. cover_xl is 1000x1000,
            // cover_big is 500x500. Deezer returns both for most albums.
            $album = $result['album'];
            if ( ! empty( $album['cover_xl'] ) ) {
                return (string) $album['cover_xl'];
            }
            if ( ! empty( $album['cover_big'] ) ) {
                return (string) $album['cover_big'];
            }
            if ( ! empty( $album['cover_medium'] ) ) {
                return (string) $album['cover_medium'];
            }
            if ( ! empty( $album['cover'] ) ) {
                return (string) $album['cover'];
            }
        }

        return '';
    }

    /**
     * Escape a value for inclusion inside Deezer's advanced query
     * syntax. Double quotes and backslashes need to be escaped so a
     * title with quotation marks does not break out of its field.
     *
     * @param string $value Raw value.
     * @return string
     */
    private static function escape_query_value( $value ) {
        return str_replace( array( '\\', '"' ), array( '\\\\', '\\"' ), (string) $value );
    }

    /**
     * Normalize a string for loose comparison. Same rules as the
     * iTunes lookup uses so the two sources agree on what counts as
     * a match.
     *
     * @param string $value Raw value.
     * @return string
     */
    private static function normalize_for_compare( $value ) {
        $value = strtolower( trim( (string) $value ) );
        $value = preg_replace( '/\s*\(feat\.?[^)]+\)/i', '', $value );
        $value = preg_replace( '/\s*feat\.?\s+.*$/i', '', $value );
        $value = preg_replace( '/[^\p{L}\p{N}]+/u', ' ', $value );
        $value = preg_replace( '/\s+/', ' ', $value );
        return trim( (string) $value );
    }
}
