package kz.konturai.parser.service;

import kz.konturai.parser.dto.SerperSearchItem;
import kz.konturai.parser.dto.SerperSearchResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.core.ParameterizedTypeReference;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;

import java.net.URI;
import java.time.Duration;
import java.util.*;

@Service
public class SerperSearchService {
    private static final Logger log = LoggerFactory.getLogger(SerperSearchService.class);

    private final WebClient webClient;

    @Value("${serper.api.key:}")
    private String apiKey;

    @Value("${serper.api.url:https://google.serper.dev}")
    private String apiUrl;

    @Value("${serper.timeoutMs:30000}")
    private long timeoutMs;

    @Value("${serper.retry.maxAttempts:3}")
    private int maxRetryAttempts;

    @Value("${serper.retry.initialDelayMs:1000}")
    private long initialRetryDelayMs;

    @Value("${serper.retry.maxDelayMs:10000}")
    private long maxRetryDelayMs;

    @Value("${serper.retry.multiplier:2.0}")
    private double retryMultiplier;

    public SerperSearchService() {
        this.webClient = WebClient.builder()
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }

    public SerperSearchResult search(String query) {
        return search(query, 10, "kz", "ru");
    }

    public SerperSearchResult search(String query, int num, String gl, String hl) {
        if (query == null || query.isBlank()) {
            return new SerperSearchResult(query, "ERROR", "Empty query", List.of());
        }
        if (apiKey == null || apiKey.isBlank()) {
            return new SerperSearchResult(query, "MISSING_API_KEY",
                    "Serper API key is not configured (serper.api.key is blank)", List.of());
        }

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("q", query);
        body.put("num", Math.max(1, Math.min(num, 20)));
        if (gl != null && !gl.isBlank()) {
            body.put("gl", gl);
        }
        if (hl != null && !hl.isBlank()) {
            body.put("hl", hl);
        }

        try {
            Mono<Map<String, Object>> call = webClient.post()
                    .uri(apiUrl + "/search")
                    .header("X-API-KEY", apiKey)
                    .bodyValue(body)
                    .retrieve()
                    .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
                    .retryWhen(createRetrySpec("search"))
                    .onErrorResume(err -> {
                        log.warn("Serper API request failed after retries: {} - {}", err.getMessage(),
                                err.getClass().getSimpleName());
                        return Mono.empty();
                    });

            if (timeoutMs > 0) {
                call = call.timeout(Duration.ofMillis(timeoutMs));
            }

            Map<String, Object> response = call.block();

            if (response == null) {
                String msg = "Serper API request failed after all retry attempts";
                log.warn(msg);
                return new SerperSearchResult(query, "ERROR", msg, List.of());
            }

            List<SerperSearchItem> items = extractOrganicItems(response);
            return new SerperSearchResult(query, "OK", null, items);
        } catch (WebClientResponseException e) {
            String msg = "Serper API error: HTTP " + e.getStatusCode().value() + " - " + e.getResponseBodyAsString();
            log.warn(msg);
            return new SerperSearchResult(query, "ERROR", msg, List.of());
        } catch (Exception e) {
            String msg = "Failed to call Serper API: " + e.getMessage();
            log.warn(msg, e);
            return new SerperSearchResult(query, "ERROR", msg, List.of());
        }
    }

    private List<SerperSearchItem> extractOrganicItems(Map<String, Object> response) {
        if (response == null || response.isEmpty()) {
            return List.of();
        }
        Object organicObj = response.get("organic");
        if (!(organicObj instanceof List<?> organicList)) {
            return List.of();
        }

        List<SerperSearchItem> out = new ArrayList<>();
        for (Object o : organicList) {
            if (!(o instanceof Map<?, ?> m)) {
                continue;
            }
            String title = str(m.get("title"));
            String link = str(m.get("link"));
            String snippet = str(m.get("snippet"));
            String host = hostFromLink(link);
            if (title == null && link == null && snippet == null) {
                continue;
            }
            out.add(new SerperSearchItem(title, link, snippet, host));
        }
        return out;
    }

    private String str(Object v) {
        if (v == null) {
            return null;
        }
        String s = String.valueOf(v).trim();
        return s.isEmpty() ? null : s;
    }

    private String hostFromLink(String link) {
        if (link == null || link.isBlank()) {
            return null;
        }
        try {
            URI uri = URI.create(link);
            return uri.getHost();
        } catch (Exception ignore) {
            return null;
        }
    }

    /**
     * Creates a retry specification for Serper API calls.
     * Retries on network errors (DNS failures, connection issues, timeouts) and 5xx server errors.
     */
    private RetryBackoffSpec createRetrySpec(String operation) {
        return Retry.backoff(maxRetryAttempts, Duration.ofMillis(initialRetryDelayMs))
                .maxBackoff(Duration.ofMillis(maxRetryDelayMs))
                .multiplier(retryMultiplier)
                .filter(throwable -> {
                    if (throwable instanceof WebClientResponseException) {
                        WebClientResponseException wcre = (WebClientResponseException) throwable;
                        int statusCode = wcre.getStatusCode().value();
                        // Retry on 5xx server errors and 429 rate limits
                        return statusCode == 429 || statusCode >= 500;
                    }
                    // Retry on network errors (DNS failures, connection issues, timeouts)
                    return throwable instanceof java.util.concurrent.TimeoutException
                            || throwable instanceof java.net.ConnectException
                            || throwable instanceof java.net.UnknownHostException
                            || throwable instanceof java.io.IOException
                            || throwable.getMessage() != null && (
                                    throwable.getMessage().contains("Failed to resolve")
                                    || throwable.getMessage().contains("Name resolution")
                                    || throwable.getMessage().contains("DNS"));
                })
                .doBeforeRetry(retrySignal -> {
                    long attempt = retrySignal.totalRetries() + 1;
                    Throwable failure = retrySignal.failure();
                    
                    if (failure instanceof WebClientResponseException) {
                        WebClientResponseException wcre = (WebClientResponseException) failure;
                        int statusCode = wcre.getStatusCode().value();
                        log.warn("Serper API returned {} for operation '{}'. Will retry (attempt {}/{})",
                                statusCode, operation, attempt, maxRetryAttempts);
                    } else {
                        log.warn("Serper API network error for operation '{}' ({}). Will retry (attempt {}/{})",
                                operation, failure.getClass().getSimpleName(), attempt, maxRetryAttempts);
                    }
                })
                .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
                    log.error("Serper API request for operation '{}' exhausted all {} retry attempts. Giving up.",
                            operation, maxRetryAttempts);
                    return retrySignal.failure();
                });
    }
}


