package kz.konturai.parser.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import jakarta.annotation.PostConstruct;

@Service
public class OpenAiChartService {

    private static final Logger logger = LoggerFactory.getLogger(OpenAiChartService.class);

    private final WebClient webClient;

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

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

    @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.rateLimit.maxConcurrentRequests:2}")
    private int maxConcurrentRequests;

    private Semaphore requestSemaphore;

    public OpenAiChartService(
            @Value("${openai.api.url:https://api.openai.com/v1/chat/completions}") String apiUrl,
            @Value("${openai.api.key:}") String apiKey) {

        if (apiKey == null || apiKey.isBlank()) {
            logger.warn("OpenAI API key is not configured. OpenAiChartService will not work.");
        }

        System.out.println("OpenAI API key: " + apiKey);
        System.out.println("OpenAI API url: " + apiUrl);
        System.out.println("OpenAI model name: " + modelName);
        System.out.println("OpenAI timeoutMs: " + timeoutMs);

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

    @PostConstruct
    public void initialize() {
        this.requestSemaphore = new Semaphore(maxConcurrentRequests, true);
        logger.info("OpenAiChartService initialized with max concurrent requests: {}", maxConcurrentRequests);
    }

    public Mono<String> getChartDataJson(String aggregatedLearnings) {
        return getChartDataJson(aggregatedLearnings, "ru");
    }

    public Mono<String> getChartDataJson(String aggregatedLearnings, String language) {
        // 2. Логируем начало важной операции
        logger.info("Requesting JSON data for charts from OpenAI...");
        String prompt = buildChartDataPrompt(aggregatedLearnings, language);
        return callChatCompletions("Chart Data Extraction", prompt);
    }

    public Mono<String> getChartSvg(String jsonData) {
        return getChartSvg(jsonData, "ru");
    }

    public Mono<String> getChartSvg(String jsonData, String language) {
        // 3. Логируем параметры, с которыми вызывается метод
        logger.info("Requesting SVG code for chart from OpenAI...");
        logger.info("SVG generation request based on JSON: {}", jsonData); // DEBUG, чтобы не засорять логи
        String prompt = buildSvgPrompt(jsonData, language);
        return callChatCompletions("SVG Generation", prompt);
    }

    private Mono<String> callChatCompletions(String taskName, String prompt) {
        Map<String, Object> body = new HashMap<>();
        body.put("model", modelName);
        body.put("temperature", 0);
        body.put("messages", List.of(
                Map.of("role", "user", "content", prompt)));

        logger.info("Calling OpenAI API for task '{}' with model '{}'.", taskName, modelName);
        logger.info("OpenAI Prompt: {}", prompt);

        // Acquire semaphore permit before making request
        return Mono.fromCallable(() -> {
            try {
                if (!requestSemaphore.tryAcquire(30, TimeUnit.SECONDS)) {
                    logger.warn("Timeout waiting for OpenAI request slot for task '{}'. Skipping request.", taskName);
                    return null;
                }
                return requestSemaphore;
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                logger.warn("Interrupted while waiting for OpenAI request slot for task '{}'", taskName);
                return null;
            }
        })
                .flatMap(semaphore -> {
                    if (semaphore == null) {
                        return Mono.empty();
                    }
                    return this.webClient.post()
                            .contentType(MediaType.APPLICATION_JSON)
                            .accept(MediaType.APPLICATION_JSON)
                            .bodyValue(body)
                            .retrieve()
                            .bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
                            })
                            .timeout(Duration.ofMillis(timeoutMs))
                            .retryWhen(createRetrySpec(taskName))
                            .doFinally(signalType -> {
                                // Always release semaphore permit
                                semaphore.release();
                            })
                            .doOnSuccess(response -> {
                                // 4. Логируем успешный, но еще не обработанный ответ
                                logger.info("Raw OpenAI response for task '{}': {}", taskName, response);
                            })
                            .map(resp -> {
                                try {
                                    List<Map<String, Object>> choices = (List<Map<String, Object>>) resp.get("choices");
                                    if (choices == null || choices.isEmpty()) {
                                        // 5. Логируем аномальные, но не ошибочные ситуации
                                        logger.warn("OpenAI response for task '{}' contained no 'choices'.", taskName);
                                        return null;
                                    }
                                    Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
                                    if (message == null) {
                                        logger.warn("OpenAI choice for task '{}' contained no 'message'.", taskName);
                                        return null;
                                    }
                                    Object content = message.get("content");
                                    if (content == null) {
                                        logger.warn("OpenAI message for task '{}' contained no 'content'.", taskName);
                                        return null;
                                    }
                                    String responseContent = String.valueOf(content);
                                    logger.info(
                                            "Successfully extracted content from OpenAI for task '{}'. Content length: {}",
                                            taskName, responseContent.length());
                                    return responseContent;
                                } catch (Exception e) {
                                    // 6. Логируем ошибку парсинга, если структура ответа неожиданная
                                    logger.error("Failed to parse OpenAI response for task '{}'. Response body: {}",
                                            taskName, resp,
                                            e);
                                    return null;
                                }
                            })
                            .doOnError(e -> {
                                // 7. Логируем ошибки сети или HTTP-статусов (4xx, 5xx)
                                if (e instanceof WebClientResponseException) {
                                    WebClientResponseException wcre = (WebClientResponseException) e;
                                    logger.error("Error from OpenAI API for task '{}'. Status: {}, Body: {}",
                                            taskName, wcre.getStatusCode(), wcre.getResponseBodyAsString(), e);
                                } else {
                                    logger.error("Generic WebClient error for task '{}'.", taskName, e);
                                }
                            })
                            .onErrorResume(e -> Mono.empty()); // В случае любой ошибки возвращаем пустой результат,
                                                               // чтобы не
                                                               // прерывать цепочку вызовов
                });
    }

    private String buildChartDataPrompt(String learningsAsString, String language) {
        boolean isRussian = "ru".equalsIgnoreCase(language) || "russian".equalsIgnoreCase(language);

        if (isRussian) {
            return "Ты — AI-аналитик данных. Внимательно проанализируй следующий текст. Найди в нём все наборы числовых данных, которые можно представить в виде простых диаграмм (столбчатых или круговых). Для КАЖДОГО найденного набора данных создай JSON-объект. Верни ТОЛЬКО JSON-массив этих объектов. Структура каждого объекта: {\\\"chartType\\\": \\\"bar\\\" или \\\"pie\\\", \\\"title\\\": \\\"Название диаграммы\\\", \\\"labels\\\": [\\\"Метка 1\\\"], \\\"data\\\": [число1]}. Если подходящих данных нет, верни пустой массив []. Текст для анализа:\n---\n"
                    + learningsAsString + "\n---";
        } else {
            return "You are an AI data analyst. Carefully analyze the following text. Find all sets of numerical data suitable for simple bar or pie charts. For EACH data set you find, create a JSON object."
                    +
                    " Return ONLY a JSON array of these objects." +

                    " The structure of each object MUST STRICTLY be: " +
                    "{\"chartType\": \"...\", \"title\": \"Chart Title\", \"labels\": [\"Label 1\"], \"data\": [number1]}."
                    +

                    "\n\nCRITICAL RULES:\n" +
                    "1. For 'chartType', use 'pie' if the data represents parts of a whole (like percentages or market shares). In all other cases, use 'bar'.\n"
                    +
                    "2. The 'data' array MUST contain ONLY NUMBERS. Do not include text, units, or placeholders. Convert all values to simple numbers.\n"
                    +
                    "3. If a numerical value is not available for a label, you MUST use the number 0 as a placeholder.\n"
                    +
                    "4. DO NOT omit any keys.\n\n" +

                    "Example of a correct response:\n" +
                    "[{\"chartType\":\"pie\",\"title\":\"Market Share\",\"labels\":[\"Company A\",\"Company B\"],\"data\":[60,40]}]"
                    +
                    "\n\n" +
                    "If no suitable data is found, return an empty array [].\n\n" +
                    "Text to analyze:\n---\n" +
                    learningsAsString + "\n---";
        }
    }

    private String buildChartDataPromptRussian(String learningsAsString) {
        return buildChartDataPrompt(learningsAsString, "ru");
    }

    private String buildSvgPrompt(String json, String language) {
        boolean isRussian = "ru".equalsIgnoreCase(language) || "russian".equalsIgnoreCase(language);

        if (isRussian) {
            return "Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. "
                    +
                    "SVG должен быть стильным, читаемым и оптимизированным для PDF, с подписями на русском языке. \n" +
                    "КРИТИЧЕСКИ ВАЖНЫЕ ПРАВИЛА:\n" +
                    "1. ВСЕ значения атрибутов width, height, x, y, r ДОЛЖНЫ быть СТРОГО ПОЛОЖИТЕЛЬНЫМИ числами (> 0).\n"
                    +
                    "2. НИКОГДА не используй отрицательные значения в атрибутах элементов <rect>, <circle>, <ellipse> и других.\n"
                    +
                    "3. Для столбчатых диаграмм: y-координата = (высота_диаграммы - высота_столбца), высота столбца всегда > 0.\n"
                    +
                    "4. Проверь каждый элемент перед отправкой - отрицательные значения приведут к ошибке конвертации.\n"
                    +
                    "5. ОПТИМИЗАЦИЯ ДЛЯ PDF:\n" +
                    "   - Используй размеры SVG: width='800' height='600' для оптимального качества в PDF\n" +
                    "   - Добавь достаточные отступы (padding) вокруг диаграммы (минимум 60px)\n" +
                    "   - Используй четкие, контрастные цвета\n" +
                    "   - Размер шрифта должен быть не менее 14px для читаемости\n" +
                    "   - Добавь рамку вокруг диаграммы для лучшего внешнего вида\n" +
                    "6. СТИЛИЗАЦИЯ:\n" +
                    "   - Используй градиенты или тени для объемности\n" +
                    "   - Добавь сетку или оси с подписями\n" +
                    "   - Используй разные цвета для разных элементов\n" +
                    "7. Не добавляй никаких комментариев или объяснений.\n" +
                    "8. Верни ТОЛЬКО SVG-код, начиная с тега <svg> и заканчивая </svg>.\n\n" +
                    "JSON с данными:\n---\n" +
                    json + "\n---";
        } else {
            return "You are a data visualization expert that generates clean, valid SVG code optimized for PDF.\n" +
                    "Based on the following JSON, generate a complete and valid SVG code for a chart.\n\n" +

                    "CRITICAL RULES - FOLLOW EXACTLY:\n" +
                    "1. The SVG coordinate system starts with (0,0) in the TOP-LEFT corner. The Y-axis points DOWN.\n" +
                    "2. For bar charts: y-coordinate = (chart_height - bar_height), height attribute must ALWAYS be POSITIVE (> 0).\n"
                    +
                    "3. ALL dimensional attributes (width, height, x, y, r) MUST be POSITIVE numbers (> 0).\n" +
                    "4. NEVER use negative values in any element attributes - this will cause conversion errors.\n" +
                    "5. Double-check every <rect>, <circle>, <ellipse> element for positive values.\n" +
                    "6. PDF OPTIMIZATION:\n" +
                    "   - Use SVG dimensions: width='800' height='600' for optimal PDF quality\n" +
                    "   - Add sufficient padding around the chart (minimum 60px)\n" +
                    "   - Use clear, contrasting colors\n" +
                    "   - Font size should be at least 14px for readability\n" +
                    "   - Add a border around the chart for better appearance\n" +
                    "7. STYLING:\n" +
                    "   - Use gradients or shadows for depth\n" +
                    "   - Add grid lines or axes with labels\n" +
                    "   - Use different colors for different elements\n" +
                    "8. The chart must be visually appealing, with appropriate padding, colors, and English labels.\n" +
                    "9. Return ONLY the raw SVG code, starting with <svg> and ending with </svg>. No explanations or markdown.\n\n"
                    +

                    "JSON data:\n---\n" +
                    json + "\n---";
        }
    }

    private String buildSvgPromptRussian(String json) {
        return buildSvgPrompt(json, "ru");
    }

    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();
                        // Only retry on 429 (rate limit) and 5xx (server errors)
                        // Don't retry on 401, 403, etc.
                        return statusCode == 429 || statusCode >= 500;
                    }
                    // Retry on network errors (timeouts, connection issues)
                    return throwable instanceof java.util.concurrent.TimeoutException
                            || throwable instanceof java.net.ConnectException
                            || throwable instanceof java.io.IOException;
                })
                .doBeforeRetry(retrySignal -> {
                    long attempt = retrySignal.totalRetries() + 1;
                    Throwable failure = retrySignal.failure();

                    Duration retryAfter = null;
                    if (failure instanceof WebClientResponseException) {
                        WebClientResponseException wcre = (WebClientResponseException) failure;
                        if (wcre.getStatusCode().value() == 429) {
                            // Try to extract Retry-After header
                            String retryAfterHeader = wcre.getHeaders().getFirst("Retry-After");
                            if (retryAfterHeader != null) {
                                try {
                                    int seconds = Integer.parseInt(retryAfterHeader);
                                    retryAfter = Duration.ofSeconds(seconds);
                                    logger.warn(
                                            "OpenAI API returned 429 for operation '{}'. Retry-After: {} seconds. Will retry in {}ms (attempt {}/{})",
                                            operation, seconds, retryAfter.toMillis(), attempt, maxRetryAttempts);
                                } catch (NumberFormatException e) {
                                    // Ignore if header is not a number
                                }
                            }
                        }
                    }

                    if (retryAfter == null) {
                        logger.warn("OpenAI API returned error for operation '{}'. Will retry (attempt {}/{})",
                                operation, attempt, maxRetryAttempts);
                    }
                })
                .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
                    logger.error("OpenAI request for operation '{}' exhausted all {} retry attempts. Giving up.",
                            operation, maxRetryAttempts);
                    return retrySignal.failure();
                });
    }

}