NewsService.java

package com.wavii.service;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wavii.dto.news.NewsArticleDto;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriUtils;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@Slf4j
public class NewsService {

    private final RestTemplate restTemplate = new RestTemplate();

    @Value("${newsdata.api-key}")
    private String apiKey;

    @Value("${newsdata.base-url}")
    private String baseUrl;

    /**
     * Obtiene artículos de noticias musicales desde NewsData.io con sistema de caché.
     * Si la búsqueda principal no devuelve resultados, intenta con términos alternativos.
     * 
     * @param query Término de búsqueda (ej. "guitarra", "rock").
     * @param language Código de idioma (ej. "es", "en").
     * @param size Cantidad máxima de artículos a recuperar.
     * @return Lista de artículos de noticias encontrados.
     */
    @Cacheable(
            value = "news",
            key = "T(String).format('%s|%s|%s', #query ?: '', #language ?: '', #size)",
            unless = "#result == null || #result.isEmpty()"
    )
    public List<NewsArticleDto> fetchMusicNews(String query, String language, int size) {
        if (apiKey == null || apiKey.isBlank()) {
            log.warn("NEWSDATA_API_KEY no configurada");
            return Collections.emptyList();
        }

        int safeSize = Math.min(Math.max(size, 1), 10);
        List<String> fallbackQueries = buildFallbackQueries(query);

        try {
            for (String currentQuery : fallbackQueries) {
                List<NewsArticleDto> articles = requestNews(currentQuery, language, safeSize, true);
                if (!articles.isEmpty()) {
                    return articles;
                }

                articles = requestNews(currentQuery, language, safeSize, false);
                if (!articles.isEmpty()) {
                    return articles;
                }
            }

            log.info("NewsData.io no devolvio resultados para query={} ni para sus alternativas", query);
            return Collections.emptyList();
        } catch (Exception e) {
            log.error("Error llamando a NewsData.io: {}", e.getMessage(), e);
            return Collections.emptyList();
        }
    }

    private List<NewsArticleDto> requestNews(String query, String language, int size, boolean titleOnly) throws Exception {
        String encodedQuery = UriUtils.encodeQueryParam(query, StandardCharsets.UTF_8);
        StringBuilder sb = new StringBuilder(baseUrl)
                .append("/latest")
                .append("?apikey=").append(apiKey)
                .append(titleOnly ? "&qInTitle=" : "&q=").append(encodedQuery)
                .append("&size=").append(size);

        if (language != null && !language.isBlank()) {
            sb.append("&language=").append(language);
        }

        String url = sb.toString();
        log.info(
                "Llamando a NewsData.io con {}: {}",
                titleOnly ? "qInTitle" : "q",
                url.replace(apiKey, "***")
        );

        String raw = restTemplate.getForObject(url, String.class);
        if (raw == null) {
            log.warn("NewsData.io devolvio respuesta nula para query={}", query);
            return Collections.emptyList();
        }

        NewsDataResponse response = new ObjectMapper().readValue(raw, NewsDataResponse.class);

        if (!"success".equals(response.getStatus())) {
            log.warn("NewsData.io devolvio status={}: {}", response.getStatus(), raw);
            return Collections.emptyList();
        }

        if (response.getResults() == null || response.getResults().isEmpty()) {
            log.info("NewsData.io no devolvio resultados para query={} usando {}", query, titleOnly ? "qInTitle" : "q");
            return Collections.emptyList();
        }

        List<NewsArticleDto> articles = response.getResults().stream()
                .filter(r -> r.getTitle() != null && !r.getTitle().isBlank())
                .map(r -> new NewsArticleDto(
                        r.getArticleId() != null ? r.getArticleId() : r.getLink(),
                        r.getTitle(),
                        r.getDescription(),
                        r.getLink(),
                        r.getImageUrl(),
                        r.getSourceName(),
                        r.getPubDate()
                ))
                .collect(Collectors.toList());

        log.info(
                "NewsData.io: {} articulos obtenidos para query={} usando {}",
                articles.size(),
                query,
                titleOnly ? "qInTitle" : "q"
        );
        return articles;
    }

    private List<String> buildFallbackQueries(String query) {
        String normalizedQuery = (query != null && !query.isBlank()) ? query.trim() : "music";
        String lowerQuery = normalizedQuery.toLowerCase(Locale.ROOT);
        Set<String> queries = new LinkedHashSet<>();
        queries.add(normalizedQuery);

        if (lowerQuery.contains("guitar")) {
            queries.add("guitar OR guitarist OR band OR music");
        } else if (lowerQuery.contains("piano")) {
            queries.add("piano OR pianist OR music");
        } else if (lowerQuery.contains("festival")) {
            queries.add("music festival OR concert OR live music");
        } else if (lowerQuery.contains("album") || lowerQuery.contains("release")) {
            queries.add("album OR single OR artist OR music");
        }

        queries.add("music OR musician OR band OR singer OR album OR song OR concert OR festival");
        queries.add("music");

        return new ArrayList<>(queries);
    }

    // ── Clases internas para deserializar la respuesta de NewsData.io ──

    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class NewsDataResponse {
        private String status;
        @JsonProperty("totalResults")
        private int totalResults;
        private List<NewsDataArticle> results;
    }

    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class NewsDataArticle {
        @JsonProperty("article_id")
        private String articleId;

        private String title;
        private String link;
        private String description;

        @JsonProperty("image_url")
        private String imageUrl;

        @JsonProperty("source_name")
        private String sourceName;

        @JsonProperty("pubDate")
        private String pubDate;
    }
}