package kz.konturai.parser.service;

import kz.konturai.parser.model.MarketItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.core.ParameterizedTypeReference;

import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;

import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;
import java.util.concurrent.atomic.AtomicReference;
import jakarta.annotation.PostConstruct;

@Service
public class OpenAIAnalyticsService {

    private static final Logger logger = LoggerFactory.getLogger(OpenAIAnalyticsService.class);
    private static final double DEFAULT_TEMPERATURE = 0.3;
    private static final int DEFAULT_MAX_TOKENS_SIMPLE = 1000;
    private static final int DEFAULT_MAX_TOKENS_REPORT = 12000; // базовое значение для генерации секций отчетов

    private WebClient webClient;

    @Value("${openai.model.name:gpt-4o-mini}")
    private String modelName;

    @Value("${openai.timeoutMs:90000}")
    private long timeoutMs;

    @Value("${openai.timeoutMs.instruction:180000}")
    private long timeoutMsInstruction;

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

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

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

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

    @Value("${openai.retry.rateLimitMaxAttempts:10}")
    private int rateLimitMaxAttempts;

    @Value("${openai.retry.rateLimitInitialDelayMs:5000}")
    private long rateLimitInitialDelayMs;

    @Value("${openai.retry.rateLimitMaxDelayMs:300000}")
    private long rateLimitMaxDelayMs;

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

    private String openaiUrl;

    /**
     * Helper for building OpenAI chat-completions request body (extracted for unit
     * testing).
     */
    Map<String, Object> buildChatCompletionsRequestBody(
            String model,
            List<Map<String, Object>> messages,
            int maxTokens,
            double temperature) {
        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("model", model);
        requestBody.put("messages", messages);
        requestBody.put("max_tokens", maxTokens);
        requestBody.put("temperature", temperature);
        return requestBody;
    }

    /**
     * Helper for building chat messages list (system + user) (extracted for unit
     * testing).
     */
    List<Map<String, Object>> buildChatMessages(String userPrompt, String systemPrompt) {
        List<Map<String, Object>> messages = new ArrayList<>();
        if (systemPrompt != null && !systemPrompt.isBlank()) {
            Map<String, Object> systemMessage = new HashMap<>();
            systemMessage.put("role", "system");
            systemMessage.put("content", systemPrompt);
            messages.add(systemMessage);
        }

        Map<String, Object> userMessage = new HashMap<>();
        userMessage.put("role", "user");
        userMessage.put("content", userPrompt);
        messages.add(userMessage);
        return messages;
    }

    public OpenAIAnalyticsService(
            @Value("${openai.api.url:https://api.openai.com/v1/chat/completions}") String openaiUrl) {
        this.openaiUrl = openaiUrl;
    }

    @PostConstruct
    public void initialize() {
        // Проверяем API ключ при инициализации
        if (apiKey == null || apiKey.trim().isEmpty()) {
            logger.error(
                    "OpenAI API key is not configured properly. Please set OPENAI_API_KEY environment variable or update application.properties");
            throw new IllegalStateException("OpenAI API key is not configured");
        }

        if (!apiKey.startsWith("sk-")) {
            logger.warn("OpenAI API key does not start with 'sk-'. Please verify your API key is correct.");
        }

        HttpClient httpClient = HttpClient.create()
                .compress(true)
                .responseTimeout(Duration.ofMillis(timeoutMs));

        this.webClient = WebClient.builder()
                .baseUrl(openaiUrl)
                .clientConnector(new ReactorClientHttpConnector(httpClient))
                .defaultHeader("Authorization", "Bearer " + apiKey)
                .build();

        logger.info("OpenAIAnalyticsService initialized with model: {}", modelName);
    }

    public MarketItem.Analytics analyzeText(String text) {
        return analyzeText(text, "ru");
    }

    public MarketItem.Analytics analyzeText(String text, String language) {
        MarketItem.Analytics analytics = new MarketItem.Analytics();
        boolean isRussian = "ru".equalsIgnoreCase(language) || "russian".equalsIgnoreCase(language);

        try {
            String summaryPrompt = isRussian
                    ? "Напиши краткое саммари следующей новостной статьи на русском языке. Ответ должен содержать только саммари из 3-4 предложений, без лишних вступлений. Статья: "
                    : "Write a brief summary of the following news article in English. The response should contain only a summary of 3-4 sentences, without any unnecessary introductions. Article: ";
            String summary = generate(text, summaryPrompt);
            analytics.setSummary(summary);
        } catch (Exception e) {
            logger.warn("Failed to get summary from OpenAI: {}", e.getMessage());
        }

        try {
            String tagsPrompt = isRussian
                    ? "Извлеки 5-7 ключевых слов или тегов из текста новостной статьи. В ответе дай только список тегов через запятую, без нумерации и заголовков. Статья: "
                    : "Extract 5-7 key words or tags from the news article text. In your response, provide only a list of tags separated by commas, without numbering or headers. Article: ";
            String tagsCsv = generate(text, tagsPrompt);
            String[] tags = tagsCsv == null ? new String[] {} : tagsCsv.replace("\n", " ").split("\\s*,\\s*");
            analytics.setTags(tags);
        } catch (Exception e) {
            logger.warn("Failed to get tags from OpenAI: {}", e.getMessage());
        }

        try {
            String sentimentPrompt = isRussian
                    ? "Определи тональность текста новостной статьи. В ответе дай только одно слово латиницей: positive, negative или neutral. Статья: "
                    : "Determine the sentiment of the news article text. In your response, provide only one word in English: positive, negative, or neutral. Article: ";
            String sentiment = generate(text, sentimentPrompt);
            if (sentiment != null) {
                sentiment = sentiment.trim().toLowerCase();
            }
            analytics.setSentiment(sentiment);
        } catch (Exception e) {
            logger.warn("Failed to get sentiment from OpenAI: {}", e.getMessage());
        }

        try {
            String entitiesPrompt = isRussian
                    ? "Извлеки из текста имена людей, названия компаний и географические локации. В ответе дай только JSON объект следующей структуры: {\"persons\": [], \"companies\": [], \"locations\": []}. Статья: "
                    : "Extract from the text names of people, company names, and geographic locations. In your response, provide only a JSON object with the following structure: {\"persons\": [], \"companies\": [], \"locations\": []}. Article: ";
            String entitiesJson = generate(text, entitiesPrompt);
            Map<String, Object> entities = new HashMap<>();
            if (entitiesJson != null && entitiesJson.trim().startsWith("{")) {
                try {
                    ObjectMapper mapper = new ObjectMapper();
                    entities = mapper.readValue(entitiesJson, new TypeReference<Map<String, Object>>() {
                    });
                } catch (Exception parseEx) {
                    entities.put("raw", entitiesJson);
                }
            }
            analytics.setEntities(entities);
        } catch (Exception e) {
            logger.warn("Failed to get entities from OpenAI: {}", e.getMessage());
        }

        return analytics;
    }

    private String generate(String text, String instructionPrefix) {
        String prompt = instructionPrefix + text;

        Map<String, Object> message = new HashMap<>();
        message.put("role", "user");
        message.put("content", prompt);

        Map<String, Object> requestBody = buildChatCompletionsRequestBody(
                modelName,
                List.of(message),
                DEFAULT_MAX_TOKENS_SIMPLE,
                DEFAULT_TEMPERATURE);

        try {
            Mono<Map<String, Object>> responseMono = this.webClient.post()
                    .contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .bodyValue(requestBody)
                    .retrieve()
                    .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
                    })
                    .timeout(Duration.ofMillis(timeoutMs))
                    .retryWhen(createRetrySpec("generate"))
                    .onErrorResume(err -> {
                        logger.error("OpenAI request failed after retries: {} - {}", err.getMessage(),
                                err.getClass().getSimpleName());
                        if (err instanceof WebClientResponseException) {
                            WebClientResponseException wcre = (WebClientResponseException) err;
                            if (wcre.getStatusCode().value() == 401) {
                                logger.error(
                                        "OpenAI API key is invalid or expired. Please check your openai.api.key configuration.");
                            } else if (wcre.getStatusCode().value() == 429) {
                                logger.error("OpenAI API rate limit exceeded after all retry attempts.");
                            } else if (wcre.getStatusCode().value() >= 500) {
                                logger.error("OpenAI API server error after all retry attempts.");
                            }
                        } else if (err instanceof java.util.concurrent.TimeoutException) {
                            logger.error("OpenAI API request timed out after {}ms", timeoutMs);
                        }
                        return Mono.empty();
                    });
            // Calculate block timeout: request timeout + (max retries * max delay) + buffer
            long blockTimeoutMs = timeoutMs
                    + (maxRetryAttempts * maxRetryDelayMs)
                    + 30000; // 30 second buffer
            Map<String, Object> response = responseMono.block(Duration.ofMillis(blockTimeoutMs));

            if (response == null) {
                return null;
            }

            @SuppressWarnings("unchecked")
            List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
            if (choices != null && !choices.isEmpty()) {
                Map<String, Object> firstChoice = choices.get(0);
                Object msg = firstChoice.get("message");
                if (msg instanceof Map) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> messageObj = (Map<String, Object>) msg;
                    Object content = messageObj.get("content");
                    return content == null ? null : String.valueOf(content).trim();
                }
            }
            return null;
        } catch (Exception e) {
            // .block() throws IllegalStateException on timeout, not TimeoutException
            if (e.getMessage() != null && e.getMessage().contains("Timeout")) {
                logger.warn("Timeout waiting for OpenAI response (block timeout): {}", e.getMessage());
            } else {
                logger.warn("Error calling OpenAI generate: {} - {}", e.getMessage(), e.getClass().getSimpleName());
            }
            return null;
        }
    }

    public String generateWithInstruction(String text, String instruction) {
        return generateWithInstruction(text, instruction, "ru");
    }

    public String generateWithInstruction(String text, String instruction, String language) {
        return generateWithInstructionWithModel(text, instruction, language, modelName);
    }

    /**
     * Генерирует текст с использованием указанной модели OpenAI
     * 
     * @param text        Текст для обработки
     * @param instruction Инструкция для AI
     * @param language    Язык ответа
     * @param modelName   Название модели OpenAI (например, "gpt-4o-mini" или
     *                    "gpt-4o")
     * @return Сгенерированный текст
     */
    public String generateWithInstructionWithModel(String text, String instruction, String language, String modelName) {
        return generateWithInstructionWithModel(text, instruction, language, modelName, null, null);
    }

    /**
     * Генерирует текст с поддержкой system prompt и override для max_tokens.
     *
     * @param text              Текст (контекст), который будет добавлен к
     *                          instruction
     * @param instruction       Основная инструкция для модели
     * @param language          Язык ответа (исторически передается, но не влияет на
     *                          протокол)
     * @param modelName         Название модели OpenAI
     * @param systemPrompt      System prompt (если null/blank — не добавляется)
     * @param maxTokensOverride Override для max_tokens (если null — используется
     *                          дефолт)
     * @return Сгенерированный текст
     */
    public String generateWithInstructionWithModel(
            String text,
            String instruction,
            String language,
            String modelName,
            String systemPrompt,
            Integer maxTokensOverride) {
        return generateWithInstructionWithModel(text, instruction, language, modelName, systemPrompt, maxTokensOverride, null);
    }

    /**
     * Генерирует текст с поддержкой system prompt, override для max_tokens и кастомного таймаута.
     *
     * @param text              Текст (контекст), который будет добавлен к
     *                          instruction
     * @param instruction       Основная инструкция для модели
     * @param language          Язык ответа (исторически передается, но не влияет на
     *                          протокол)
     * @param modelName         Название модели OpenAI
     * @param systemPrompt      System prompt (если null/blank — не добавляется)
     * @param maxTokensOverride Override для max_tokens (если null — используется
     *                          дефолт)
     * @param timeoutOverrideMs Override для таймаута в миллисекундах (если null — используется
     *                          timeoutMsInstruction)
     * @return Сгенерированный текст
     */
    public String generateWithInstructionWithModel(
            String text,
            String instruction,
            String language,
            String modelName,
            String systemPrompt,
            Integer maxTokensOverride,
            Long timeoutOverrideMs) {
        String prompt = instruction + "\n\n" + text;

        List<Map<String, Object>> messages = buildChatMessages(prompt, systemPrompt);

        int maxTokens = maxTokensOverride != null ? maxTokensOverride : DEFAULT_MAX_TOKENS_REPORT;
        long timeoutMsToUse = timeoutOverrideMs != null ? timeoutOverrideMs : timeoutMsInstruction;
        
        Map<String, Object> requestBody = buildChatCompletionsRequestBody(
                modelName,
                messages,
                maxTokens,
                DEFAULT_TEMPERATURE);

        try {
            logger.debug("Calling OpenAI API with model: {} (timeout: {}ms)", modelName, timeoutMsToUse);
            // Use timeout operator in the reactive chain so timeouts can be retried
            Mono<Map<String, Object>> responseMono = this.webClient.post()
                    .contentType(MediaType.APPLICATION_JSON)
                    .accept(MediaType.APPLICATION_JSON)
                    .bodyValue(requestBody)
                    .retrieve()
                    .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
                    })
                    .timeout(Duration.ofMillis(timeoutMsToUse))
                    .retryWhen(createRetrySpec("generateWithInstruction"))
                    .onErrorResume(err -> {
                        logger.error("OpenAI request failed after retries: {} - {}", err.getMessage(),
                                err.getClass().getSimpleName());
                        if (err instanceof WebClientResponseException) {
                            WebClientResponseException wcre = (WebClientResponseException) err;
                            if (wcre.getStatusCode().value() == 401) {
                                logger.error(
                                        "OpenAI API key is invalid or expired. Please check your openai.api.key configuration.");
                            } else if (wcre.getStatusCode().value() == 429) {
                                logger.error("OpenAI API rate limit exceeded after all retry attempts.");
                            } else if (wcre.getStatusCode().value() >= 500) {
                                logger.error("OpenAI API server error after all retry attempts.");
                            }
                        } else if (err instanceof java.util.concurrent.TimeoutException) {
                            logger.error("OpenAI API request timed out after {}ms", timeoutMsToUse);
                        }
                        return Mono.empty();
                    });
            // Calculate block timeout: request timeout + (max retries * max delay) + buffer
            // This ensures we can wait through all retry attempts with rate limit delays
            long blockTimeoutMs = timeoutMsToUse
                    + (rateLimitMaxAttempts * rateLimitMaxDelayMs)
                    + 30000; // 30 second buffer
            logger.debug("Block timeout calculated: {}ms (request: {}ms, retries: {} * {}ms, buffer: 30000ms)",
                    blockTimeoutMs, timeoutMsToUse, rateLimitMaxAttempts, rateLimitMaxDelayMs);
            Map<String, Object> response = responseMono.block(Duration.ofMillis(blockTimeoutMs));

            if (response == null) {
                return null;
            }

            @SuppressWarnings("unchecked")
            List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
            if (choices != null && !choices.isEmpty()) {
                Map<String, Object> firstChoice = choices.get(0);
                Object msg = firstChoice.get("message");
                if (msg instanceof Map) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> messageObj = (Map<String, Object>) msg;
                    Object content = messageObj.get("content");
                    return content == null ? null : String.valueOf(content).trim();
                }
            }
            return null;
        } catch (Exception e) {
            // .block() throws IllegalStateException on timeout, not TimeoutException
            if (e.getMessage() != null && e.getMessage().contains("Timeout")) {
                logger.warn("Timeout waiting for OpenAI response (block timeout): {}", e.getMessage());
            } else {
                logger.warn("Error calling OpenAI generate (generic): {} - {}", e.getMessage(),
                        e.getClass().getSimpleName());
            }
            return null;
        }
    }

    private RetryBackoffSpec createRetrySpec(String operation) {
        // Store Retry-After duration for 429 errors
        AtomicReference<Duration> retryAfterDuration = new AtomicReference<>(null);

        // Use the higher max attempts to handle rate limit errors better
        // This will apply to all retries, but rate limit errors need more attempts
        int effectiveMaxAttempts = Math.max(maxRetryAttempts, rateLimitMaxAttempts);

        return Retry.backoff(effectiveMaxAttempts, Duration.ofMillis(initialRetryDelayMs))
                .maxBackoff(Duration.ofMillis(maxRetryDelayMs))
                .multiplier(retryMultiplier)
                .filter(throwable -> {
                    if (throwable instanceof WebClientResponseException) {
                        WebClientResponseException wcre = (WebClientResponseException) throwable;
                        int statusCode = wcre.getStatusCode().value();

                        if (statusCode == 429) {
                            // Extract Retry-After header for 429 errors
                            String retryAfterHeader = wcre.getHeaders().getFirst("Retry-After");
                            if (retryAfterHeader != null) {
                                try {
                                    int seconds = Integer.parseInt(retryAfterHeader);
                                    // Add 10% buffer to be safe, but cap at max delay
                                    int delaySeconds = (int) Math.min(seconds * 1.1, rateLimitMaxDelayMs / 1000);
                                    retryAfterDuration.set(Duration.ofSeconds(delaySeconds));
                                    logger.warn(
                                            "OpenAI API returned 429 for operation '{}'. Retry-After: {} seconds. Will wait {}ms before retry.",
                                            operation, seconds, retryAfterDuration.get().toMillis());
                                } catch (NumberFormatException e) {
                                    // Ignore if header is not a number
                                    retryAfterDuration.set(null);
                                }
                            } else {
                                retryAfterDuration.set(null);
                            }
                        } else {
                            retryAfterDuration.set(null);
                        }

                        // Only retry on 429 (rate limit) and 5xx (server errors)
                        return statusCode == 429 || statusCode >= 500;
                    }
                    // Retry on network errors (timeouts, connection issues)
                    retryAfterDuration.set(null);
                    boolean isTimeout = throwable instanceof java.util.concurrent.TimeoutException
                            || (throwable.getMessage() != null && throwable.getMessage().contains("Timeout"));
                    if (isTimeout) {
                        logger.warn("OpenAI API timeout detected for operation '{}'. Will retry.", operation);
                    }
                    return isTimeout
                            || throwable instanceof java.net.ConnectException
                            || throwable instanceof java.io.IOException;
                })
                .doBeforeRetry(retrySignal -> {
                    long attempt = retrySignal.totalRetries() + 1;
                    Throwable failure = retrySignal.failure();

                    // If we have a Retry-After duration for 429 errors, sleep for that duration
                    // This ensures we respect the API's requested delay before Reactor applies its
                    // backoff
                    Duration customDelay = retryAfterDuration.get();
                    if (customDelay != null && failure instanceof WebClientResponseException) {
                        WebClientResponseException wcre = (WebClientResponseException) failure;
                        if (wcre.getStatusCode().value() == 429) {
                            try {
                                logger.warn(
                                        "Respecting Retry-After header: sleeping for {}ms (attempt {}/{})",
                                        customDelay.toMillis(), attempt, rateLimitMaxAttempts);
                                Thread.sleep(customDelay.toMillis());
                                retryAfterDuration.set(null); // Reset after use
                            } catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                                logger.warn("Interrupted while waiting for Retry-After delay");
                            }
                        }
                    }

                    // Log retry attempt
                    if (failure instanceof WebClientResponseException) {
                        WebClientResponseException wcre = (WebClientResponseException) failure;
                        int statusCode = wcre.getStatusCode().value();
                        int maxAttempts = statusCode == 429 ? rateLimitMaxAttempts : maxRetryAttempts;
                        if (statusCode == 429 && customDelay == null) {
                            logger.warn(
                                    "OpenAI API returned 429 for operation '{}'. Will retry with exponential backoff (attempt {}/{})",
                                    operation, attempt, maxAttempts);
                        } else if (statusCode != 429) {
                            logger.warn("OpenAI API returned {} for operation '{}'. Will retry (attempt {}/{})",
                                    statusCode, operation, attempt, maxAttempts);
                        }
                    } else {
                        logger.warn("OpenAI API network error for operation '{}'. Will retry (attempt {}/{})",
                                operation, attempt, maxRetryAttempts);
                    }
                })
                .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
                    Throwable failure = retrySignal.failure();
                    int maxAttempts = maxRetryAttempts;
                    if (failure instanceof WebClientResponseException) {
                        WebClientResponseException wcre = (WebClientResponseException) failure;
                        if (wcre.getStatusCode().value() == 429) {
                            maxAttempts = rateLimitMaxAttempts;
                        }
                    }
                    logger.error("OpenAI request for operation '{}' exhausted all {} retry attempts. Giving up.",
                            operation, maxAttempts);
                    return retrySignal.failure();
                });
    }

}
