package kz.konturai.parser.service;

import kz.konturai.parser.dto.MarketingAnalysisRequest;
import kz.konturai.parser.dto.MarketingAnalysisResult;
import kz.konturai.parser.dto.MarketingAnalysisResponseV2;
import kz.konturai.parser.dto.SerperSearchItem;
import kz.konturai.parser.dto.SerperSearchResult;
import kz.konturai.parser.dto.StatusHistoryEntry;
import kz.konturai.parser.model.AnalysisType;
import kz.konturai.parser.model.DetailLevel;
import kz.konturai.parser.model.MarketingAnalysis;
import kz.konturai.parser.model.MarketingAnalysisV2Document;
import kz.konturai.parser.repository.MarketingAnalysisRepository;
import kz.konturai.parser.repository.MarketingAnalysisV2Repository;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.Semaphore;
import jakarta.annotation.PostConstruct;
import org.springframework.web.reactive.function.client.WebClientResponseException;

@Service
@RequiredArgsConstructor
@Deprecated
public class MarketingAnalysisService {
    private static final String AI_MANDATORY_JSON_USAGE_WARNING = "ВНИМАНИЕ: Если в предоставленном JSON есть данные, ты ОБЯЗАН использовать их и вставить соответствующий тег [[CHART_...]]. "
            + "Никогда не пиши, что данных нет, если JSON не пустой.\n\n";
    private static final String AI_ULTIMATE_JSON_PRESENCE_INSTRUCTION = ""
            + "ИНСТРУКЦИЯ (ОБЯЗАТЕЛЬНО): Данные в JSON 100% присутствуют. Тебе ЗАПРЕЩЕНО писать, что данных нет/данные отсутствуют/нет информации.\n"
            + "Найди нужный объект в JSON, проанализируй каждую цифру/процент и вставь соответствующий тег [[CHART_...]] / [[TABLE_...]] рядом с тем абзацем, где ты обсуждаешь эти данные.\n"
            + "Если какая-то бизнес-метрика не представлена явно, используй аналитическую экстраполяцию и обозначь это как \"Экстраполяция\" (без фраз про отсутствие данных).\n\n";
    private static final String AI_PREMIUM_CONSULTING_STYLE_GUIDE = ""
            + "СТИЛЬ (ОБЯЗАТЕЛЬНО): пиши как консалтинг «Большой четверки»: структурно, уверенно, с причинно-следственной логикой.\n"
            + "Используй профессиональную лексику и леммы: «аналитическая экстраполяция», «дифференциация предложения», «конверсионный слой», «кастомизация оффера».\n"
            + "Текст должен звучать дорого: меньше общих слов, больше управленческих выводов, четких аргументов и практических шагов.\n"
            + "ФОРМАТ (ОБЯЗАТЕЛЬНО): пиши аналитический текст короткими емкими абзацами (2–4 предложения), без «воды».\n"
            + "ГРАФИКИ (ОБЯЗАТЕЛЬНО): располагай теги графиков (например, [[CHART_...]]) непосредственно внутри или сразу после описания конкретных данных/цифр, к которым относится график.\n\n";

    private static final int REPORT_DETAILED_MAX_TOKENS = 12000;

    private DetailLevel resolveDetailLevelFromContext(Map<String, Object> contextJson) {
        if (contextJson == null) {
            return null;
        }
        Object v = contextJson.get("detailLevel");
        return DetailLevel.fromString(v == null ? null : String.valueOf(v));
    }

    private boolean isKeyVerboseSectionForHybridRule(String sectionId) {
        return "audience".equalsIgnoreCase(sectionId)
                || "competitors".equalsIgnoreCase(sectionId)
                || "strategy".equalsIgnoreCase(sectionId);
    }

    private String buildSystemPromptForSection(DetailLevel detailLevel, String sectionId) {
        if (detailLevel != DetailLevel.ПОДРОБНО) {
            return null;
        }
        return "РЕЖИМ ПОДРОБНО (SYSTEM): Проведи глубокий академический анализ с причинно-следственной логикой.\n"
                + "- Объем: 1000–1500 слов (если раздел ключевой — ближе к верхней границе).\n"
                + "- Для КАЖДОГО вывода приводи по 3 конкретных примера реализации.\n"
                + "- Обязательно включи анализ смежных рынков и долгосрочные прогнозы на 3–5 лет.\n"
                + "- Включай расчеты в LaTeX там, где уместно: CAC, LTV, ROI, Payback Period. Если не хватает чисел — делай обоснованные допущения и помечай как \"Экстраполяция\".\n"
                + "- Используй фреймворки, когда релевантно: PESTEL и Porter's Five Forces.\n"
                + "- Всегда опирайся на JSON-контекст, не выдумывай факты.";
    }

    private void appendDetailLevelDirectives(StringBuilder promptBuilder, DetailLevel detailLevel, String sectionId) {
        if (promptBuilder == null || detailLevel == null) {
            return;
        }

        promptBuilder.append("\n\n");
        promptBuilder.append("DETAIL LEVEL (OVERRIDE): ").append(detailLevel.name()).append("\n");

        if (detailLevel == DetailLevel.КРАТКО) {
            promptBuilder.append("РЕЖИМ КРАТКО (ОБЯЗАТЕЛЬНО):\n");
            promptBuilder.append("- Игнорируй любые требования по «минимум N слов», если они есть выше.\n");
            promptBuilder.append("- Объем: 150–250 слов.\n");
            promptBuilder.append(
                    "- Формат: оставь обязательный заголовок раздела (если он требуется выше), далее используй ТОЛЬКО списки (bullet points).\n");
            promptBuilder.append(
                    "- Пиши максимально лаконично, используй только bullet points, избегай вводных слов и консалтинговых оборотов.\n");
            promptBuilder.append("- Максимум 10–14 буллетов; каждый буллет 1–2 строки.\n");
            promptBuilder.append(
                    "- Плейсхолдеры [[CHART_...]]/[[TABLE_...]]: вставляй ТОЛЬКО если в JSON реально есть данные для соответствующего графика/таблицы и раздел это обсуждает. Если данных нет — не вставляй тег и не упоминай его.\n");
            return;
        }

        if (detailLevel == DetailLevel.ПОДРОБНО) {
            promptBuilder.append("РЕЖИМ ПОДРОБНО (ОБЯЗАТЕЛЬНО):\n");
            promptBuilder.append("- Дай глубокий академический анализ с причинно-следственной логикой.\n");
            promptBuilder
                    .append("- Используй терминологию (LTV, CAC, PESTEL, Porter's Five Forces) там, где уместно.\n");
            promptBuilder.append("- Объем: 1000–1500 слов.\n");
            promptBuilder.append("- Для КАЖДОГО вывода приводи по 3 конкретных примера реализации.\n");
            promptBuilder.append("- Обязательно включи анализ смежных рынков и долгосрочные прогнозы на 3–5 лет.\n");
            promptBuilder.append(
                    "- Включай расчеты в LaTeX там, где уместно: CAC, LTV, ROI, Payback Period. Если не хватает цифр — делай обоснованные допущения и помечай блок как \"Экстраполяция\".\n");
            if ("market".equalsIgnoreCase(sectionId) || "strategy".equalsIgnoreCase(sectionId)) {
                promptBuilder.append(
                        "- ОБЯЗАТЕЛЬНО ВКЛЮЧИ ФРЕЙМВОРКИ: сделай отдельные подблоки PESTEL и «5 сил Портера» (не просто упоминание терминов).\n");
            } else if ("competitors".equalsIgnoreCase(sectionId)) {
                promptBuilder.append(
                        "- ОБЯЗАТЕЛЬНО ВКЛЮЧИ «5 сил Портера» (минимум: Rivalry/Threat of New Entrants/Substitutes/Buyer Power/Supplier Power) применительно к этой нише в Казахстане.\n");
            }
            promptBuilder.append("- После каждого крупного вывода добавляй конкретные тактические рекомендации.\n");
        }
    }

    private Map<String, Object> buildResearchNotesForSection(Map<String, Object> contextJson, String sectionId) {
        if (contextJson == null) {
            return new HashMap<>();
        }
        try {
            String jsonString = objectMapper.writeValueAsString(contextJson);

            StringBuilder instruction = new StringBuilder();
            instruction.append("Ты — аналитик-исследователь по рынку Казахстана.\n");
            instruction.append(
                    "На основе JSON (особенно researchPack, market, seasonality, competitorAnalysis) сделай КОНСПЕКТ-ЗАМЕТКИ.\n");
            instruction.append("Верни ТОЛЬКО валидный JSON без markdown/комментариев.\n\n");
            instruction.append("СТРУКТУРА JSON (строго):\n");
            instruction.append("{\n");
            instruction.append("  \"sectionId\": \"").append(sectionId).append("\",\n");
            instruction.append("  \"macro\": {\n");
            instruction.append(
                    "    \"inflation\": \"Коротко: что происходит с инфляцией в РК (2024–2025) и как это влияет на нишу\",\n");
            instruction
                    .append("    \"kzt\": \"Коротко: курс тенге/волатильность и влияние на себестоимость/импорт\",\n");
            instruction.append("    \"logistics\": \"Коротко: логистические/импортные риски, если релевантно нише\"\n");
            instruction.append("  },\n");
            instruction.append("  \"pestel\": {\n");
            instruction.append(
                    "    \"P\": [\"...\"], \"E\": [\"...\"], \"S\": [\"...\"], \"T\": [\"...\"], \"E2\": [\"...\"], \"L\": [\"...\"]\n");
            instruction.append("  },\n");
            instruction.append("  \"porter5\": {\n");
            instruction.append("    \"rivalry\": \"...\",\n");
            instruction.append("    \"newEntrants\": \"...\",\n");
            instruction.append("    \"substitutes\": \"...\",\n");
            instruction.append("    \"buyerPower\": \"...\",\n");
            instruction.append("    \"supplierPower\": \"...\"\n");
            instruction.append("  },\n");
            instruction.append(
                    "  \"citations\": [ { \"title\": \"Источник\", \"url\": \"https://...\", \"year\": \"2024/2025\" } ]\n");
            instruction.append("}\n\n");
            instruction.append("ПРАВИЛА:\n");
            instruction.append("- Если в researchPack есть ссылки — используй их в citations.\n");
            instruction.append(
                    "- Если данных нет — пометь соответствующие поля как \"Экстраполяция\" и не выдавай это за факт.\n");

            String resp = openAIAnalyticsService.generateWithInstructionWithModel(
                    jsonString,
                    instruction.toString(),
                    "ru",
                    miniModelName,
                    null,
                    2500);
            if (resp == null || resp.isBlank()) {
                return new HashMap<>();
            }
            String cleaned = cleanJsonFromMarkdown(resp);
            return objectMapper.readValue(cleaned, new TypeReference<Map<String, Object>>() {
            });
        } catch (Exception e) {
            logger.warn("buildResearchNotesForSection: failed for section {}: {}", sectionId, e.getMessage());
            return new HashMap<>();
        }
    }

    private void appendMandatoryJsonUsageWarning(StringBuilder promptBuilder) {
        if (promptBuilder != null) {
            promptBuilder.append(AI_MANDATORY_JSON_USAGE_WARNING);
        }
    }

    private void appendUltimateJsonPresenceInstruction(StringBuilder promptBuilder) {
        if (promptBuilder != null) {
            promptBuilder.append(AI_ULTIMATE_JSON_PRESENCE_INSTRUCTION);
        }
    }

    private void appendPremiumConsultingStyleGuide(StringBuilder promptBuilder) {
        if (promptBuilder != null) {
            promptBuilder.append(AI_PREMIUM_CONSULTING_STYLE_GUIDE);
        }
    }

    private String normalizePlaceholderToken(String token) {
        if (token == null) {
            return "";
        }
        // Normalize: remove whitespace, keep brackets, upper-case keyword part
        return token.replaceAll("\\s+", "").toUpperCase(Locale.ROOT);
    }

    private boolean containsChartOrTablePlaceholder(String text) {
        if (text == null || text.isBlank()) {
            return false;
        }
        // Fast path (exact canonical token)
        if (text.contains("[[CHART_") || text.contains("[[TABLE_")) {
            return true;
        }
        // Flexible path (ignore extra spaces, tolerate model formatting)
        return java.util.regex.Pattern.compile("\\[\\[\\s*(CHART|TABLE)_", java.util.regex.Pattern.CASE_INSENSITIVE)
                .matcher(text)
                .find();
    }

    private String removePlaceholderWithSurroundingNewlines(String text, String placeholderVariant) {
        if (text == null || placeholderVariant == null || placeholderVariant.isBlank()) {
            return text;
        }
        String quoted = java.util.regex.Pattern.quote(placeholderVariant);
        // Remove a placeholder placed on its own line (with surrounding optional
        // whitespace/newlines)
        String placeholderWithNewlines = "(\\r?\\n)?[ \\t]*" + quoted + "[ \\t]*(\\r?\\n)?";
        return text.replaceAll(placeholderWithNewlines, "\n");
    }

    private String buildChartJsonBlock(String chartKey, String jsonStr) {
        // Frontend parses fenced blocks by info-string `json:<key>` and expects a
        // blank line after the closing fence to avoid "sticking" text to the chart.
        // Contract:
        // ```json:<key>\n
        // <json>\n
        // ```\n\n
        return "```json:" + chartKey + "\n" + jsonStr + "\n```\n\n";
    }

    /**
     * Replace a specific placeholder variant with a JSON fenced block while keeping
     * spacing compact (avoid accidental \\n\\n\\n around inserted blocks).
     */
    private String replacePlaceholderVariantWithJsonBlock(String text, String placeholderVariant, String jsonBlock) {
        if (text == null || text.isBlank() || placeholderVariant == null || placeholderVariant.isBlank()
                || jsonBlock == null || jsonBlock.isBlank()) {
            return text;
        }

        String result = text;

        java.util.regex.Pattern standaloneLine = java.util.regex.Pattern.compile(
                "(?m)^[ \\t]*" + java.util.regex.Pattern.quote(placeholderVariant) + "[ \\t]*$");
        result = standaloneLine.matcher(result)
                .replaceAll(java.util.regex.Matcher.quoteReplacement(jsonBlock));

        java.util.regex.Pattern inline = java.util.regex.Pattern
                .compile(java.util.regex.Pattern.quote(placeholderVariant));
        java.util.regex.Matcher m = inline.matcher(result);
        StringBuilder sb = new StringBuilder(result.length() + jsonBlock.length());
        int last = 0;
        while (m.find()) {
            int start = m.start();
            int end = m.end();
            sb.append(result, last, start);

            boolean hasNewlineBefore = start > 0
                    && (result.charAt(start - 1) == '\n' || result.charAt(start - 1) == '\r');
            boolean hasNewlineAfter = end < result.length()
                    && (result.charAt(end) == '\n' || result.charAt(end) == '\r');

            String prefix = hasNewlineBefore ? "" : "\n";
            String suffix = hasNewlineAfter ? "" : "\n";

            sb.append(prefix).append(jsonBlock).append(suffix);
            last = end;
        }
        sb.append(result, last, result.length());
        return sb.toString();
    }

    private static final String[] SECTION_ROMANS = new String[] { "I", "II", "III", "IV", "V", "VI", "VII", "VIII",
            "IX", "X", "XI" };
    private static final Map<String, String> ROMAN_TO_ANCHOR_ID = Map.ofEntries(
            Map.entry("I", "section-i"),
            Map.entry("II", "section-ii"),
            Map.entry("III", "section-iii"),
            Map.entry("IV", "section-iv"),
            Map.entry("V", "section-v"),
            Map.entry("VI", "section-vi"),
            Map.entry("VII", "section-vii"),
            Map.entry("VIII", "section-viii"),
            Map.entry("IX", "section-ix"),
            Map.entry("X", "section-x"),
            Map.entry("XI", "section-xi"));

    private String addAnchorsAndHeadingLevelsForSections(String fullAnalysis) {
        if (fullAnalysis == null || fullAnalysis.isBlank()) {
            return fullAnalysis;
        }

        String result = fullAnalysis;
        for (String roman : SECTION_ROMANS) {
            String id = ROMAN_TO_ANCHOR_ID.get(roman);
            if (id == null) {
                continue;
            }

            // Match lines like: "I. Title" or "## I. Title"
            java.util.regex.Pattern p = java.util.regex.Pattern.compile(
                    "^(\\s*)(#{0,6}\\s*)?(" + java.util.regex.Pattern.quote(roman)
                            + ")\\.(\\s*\\S[^\\n\\r]*)\\s*$",
                    java.util.regex.Pattern.MULTILINE);
            java.util.regex.Matcher m = p.matcher(result);
            if (!m.find()) {
                continue;
            }

            int start = m.start();
            int lookbackStart = Math.max(0, start - 120);
            String lookback = result.substring(lookbackStart, start);
            if (lookback.contains("<a id=\"" + id + "\"></a>")) {
                continue;
            }

            String leadingWs = m.group(1) != null ? m.group(1) : "";
            String titlePart = m.group(4) != null ? m.group(4).trim() : "";
            String headerLine = "## " + roman + ". " + titlePart;
            String replacement = leadingWs + "<a id=\"" + id + "\"></a>\n" + leadingWs + headerLine;
            result = m.replaceFirst(java.util.regex.Matcher.quoteReplacement(replacement));
        }
        return result;
    }

    private Map<String, String> extractSectionTitles(String fullAnalysis) {
        Map<String, String> titles = new LinkedHashMap<>();
        if (fullAnalysis == null || fullAnalysis.isBlank()) {
            return titles;
        }
        for (String roman : SECTION_ROMANS) {
            java.util.regex.Pattern p = java.util.regex.Pattern.compile(
                    "^(?:\\s*<a\\s+id=\"[^\"]+\"\\s*></a>\\s*\\n)?\\s*(?:#{1,6}\\s*)?("
                            + java.util.regex.Pattern.quote(roman) + ")\\.(\\s*\\S[^\\n\\r]*)\\s*$",
                    java.util.regex.Pattern.MULTILINE);
            java.util.regex.Matcher m = p.matcher(fullAnalysis);
            if (m.find()) {
                String title = (m.group(2) != null ? m.group(2).trim() : "");
                titles.put(roman, roman + ". " + title);
            }
        }
        return titles;
    }

    private static final Logger logger = LoggerFactory.getLogger(MarketingAnalysisService.class);
    private static final int MAX_RETRY_ATTEMPTS = 3;
    private static final long RETRY_DELAY_MS = 1000;

    private final MarketingAnalysisRepository repository;
    private final MarketingAnalysisV2Repository v2Repository;
    private final OpenAIAnalyticsService openAIAnalyticsService;
    private final MinIOService minIOService;
    private final ResearchPdfService researchPdfService;
    private final SerperSearchService serperSearchService;
    private final ObjectMapper objectMapper = new ObjectMapper();

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

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

    @Value("${openai.rateLimit.maxConcurrentRequests:3}")
    private int maxConcurrentRequests;

    private Semaphore requestSemaphore;

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

    private boolean shouldIncludeResearchPackInSection(String section) {
        if (section == null) {
            return false;
        }
        return "summary".equalsIgnoreCase(section)
                || "product".equalsIgnoreCase(section)
                || "market".equalsIgnoreCase(section)
                || "competitors".equalsIgnoreCase(section)
                || "strategy".equalsIgnoreCase(section);
    }

    private Map<String, Object> buildResearchPack(MarketingAnalysisRequest request) {
        Map<String, Object> pack = new LinkedHashMap<>();
        pack.put("generatedAt", LocalDateTime.now().toString());
        pack.put("engine", "serper.dev");

        String niche = request != null ? request.getBusinessNiche() : null;
        String product = request != null ? request.getProduct() : null;
        List<String> regions = request != null ? request.getRegion() : null;
        String regionStr = (regions != null && !regions.isEmpty()) ? String.join(", ", regions) : "Казахстан";

        String topic = (niche != null && !niche.isBlank()) ? niche : (product != null ? product : "ниша");

        String preferredSites = "(site:stat.gov.kz OR site:kapital.kz OR site:kursiv.media OR site:forbes.kz OR site:pwc.kz OR site:kpmg.kz)";

        String retailSites = "site:2gis.kz OR site:kaspi.kz OR site:market.kz OR site:kolesa.kz OR site:krisha.kz OR site:chocofood.kz";
        List<Object> general = new ArrayList<>();
        String query1 = topic + " рынок " + regionStr + " 2024 2025 статистика объем рост " + preferredSites
                + retailSites;
        try {
            general.add(serperSearchService.search(query1));
        } catch (Exception e) {
            String errorType = "Unexpected error";
            if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException)
                errorType = "DNS/Network failure";
            else if (e instanceof java.util.concurrent.TimeoutException)
                errorType = "Timeout";
            else if (e instanceof java.io.IOException)
                errorType = "IO error";
            logger.warn("Serper API {} for query '{}': {}", errorType, query1, e.getMessage());
            general.add(new SerperSearchResult(query1, "ERROR", errorType + ": " + e.getMessage(), List.of()));
        }

        String query2 = "инфляция Казахстан 2024 2025 курс тенге 2024 2025 логистика импорт " + preferredSites
                + retailSites;
        try {
            general.add(serperSearchService.search(query2));
        } catch (Exception e) {
            String errorType = "Unexpected error";
            if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException)
                errorType = "DNS/Network failure";
            else if (e instanceof java.util.concurrent.TimeoutException)
                errorType = "Timeout";
            else if (e instanceof java.io.IOException)
                errorType = "IO error";
            logger.warn("Serper API {} for query '{}': {}", errorType, query2, e.getMessage());
            general.add(new SerperSearchResult(query2, "ERROR", errorType + ": " + e.getMessage(), List.of()));
        }

        Map<String, Object> followups = new LinkedHashMap<>();
        String marketQuery = topic + " объем рынка Казахстан 2024 2025 отчет " + preferredSites;
        try {
            followups.put("market", serperSearchService.search(marketQuery));
        } catch (Exception e) {
            String errorType = "Unexpected error";
            if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException)
                errorType = "DNS/Network failure";
            else if (e instanceof java.util.concurrent.TimeoutException)
                errorType = "Timeout";
            else if (e instanceof java.io.IOException)
                errorType = "IO error";
            logger.warn("Serper API {} for market query '{}': {}", errorType, marketQuery, e.getMessage());
            followups.put("market",
                    new SerperSearchResult(marketQuery, "ERROR", errorType + ": " + e.getMessage(), List.of()));
        }

        String competitorsQuery = topic + " конкуренты " + regionStr + " топ компании магазины цены";
        try {
            followups.put("competitors", serperSearchService.search(competitorsQuery));
        } catch (Exception e) {
            // Keeping existing error handling
            String errorType = "Unexpected error";
            if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException)
                errorType = "DNS/Network failure";
            else if (e instanceof java.util.concurrent.TimeoutException)
                errorType = "Timeout";
            else if (e instanceof java.io.IOException)
                errorType = "IO error";
            logger.warn("Serper API {} for competitors query '{}': {}", errorType, competitorsQuery, e.getMessage());
            followups.put("competitors",
                    new SerperSearchResult(competitorsQuery, "ERROR", errorType + ": " + e.getMessage(), List.of()));
        }

        String financeQuery = topic + " себестоимость логистика импорт курс тенге влияние 2024 2025 " + preferredSites;
        try {
            followups.put("finance", serperSearchService.search(financeQuery));
        } catch (Exception e) {
            String errorType = "Unexpected error";
            if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException)
                errorType = "DNS/Network failure";
            else if (e instanceof java.util.concurrent.TimeoutException)
                errorType = "Timeout";
            else if (e instanceof java.io.IOException)
                errorType = "IO error";
            logger.warn("Serper API {} for finance query '{}': {}", errorType, financeQuery, e.getMessage());
            followups.put("finance",
                    new SerperSearchResult(financeQuery, "ERROR", errorType + ": " + e.getMessage(), List.of()));
        }

        String incomeQuery = "средняя медианная зарплата по возрастам " + regionStr
                + " 2024 2025 статистика доходы населения бюро нацстатистики";
        try {
            followups.put("income", serperSearchService.search(incomeQuery));
        } catch (Exception e) {
            logger.warn("Serper API error for income query: {}", e.getMessage());
            // Fail silently for supplementary metrics to avoid blocking main flow
            followups.put("income", new SerperSearchResult(incomeQuery, "ERROR", e.getMessage(), List.of()));
        }

        String demandQuery = String.format("когда сезон %s в %s динамика спроса google trends", topic, regionStr);
        try {
            followups.put("demand", serperSearchService.search(demandQuery));
        } catch (Exception e) {
            logger.warn("Serper error demand: {}", e.getMessage());
        }

        String channelsQuery = String.format("%s реклама кейсы казахстан instagram tiktok kaspi маркетинг таргетолог",
                topic);
        try {
            followups.put("channels", serperSearchService.search(channelsQuery));
        } catch (Exception e) {
            logger.warn("Serper error channels: {}", e.getMessage());
        }
        pack.put("general", general);
        pack.put("followups", followups);

        // Overall status
        String status = "OK";
        for (Object o : general) {
            if (o instanceof kz.konturai.parser.dto.SerperSearchResult r) {
                if (!"OK".equalsIgnoreCase(r.status())) {
                    status = r.status();
                    break;
                }
            }
        }
        pack.put("status", status);
        return pack;
    }

    private void appendSerperEvidenceToPrompt(StringBuilder promptBuilder, String title, SerperSearchResult result,
            int limit) {
        if (promptBuilder == null) {
            return;
        }
        promptBuilder.append(title).append(":\n");
        if (result == null) {
            promptBuilder.append("- (no result)\n\n");
            return;
        }
        promptBuilder.append("- query: ").append(result.query()).append("\n");
        promptBuilder.append("- status: ").append(result.status()).append("\n");
        if (result.error() != null && !result.error().isBlank()) {
            promptBuilder.append("- error: ").append(result.error()).append("\n");
        }
        List<SerperSearchItem> items = result.items() != null ? result.items() : List.of();
        int n = Math.min(Math.max(limit, 0), items.size());
        for (int i = 0; i < n; i++) {
            SerperSearchItem it = items.get(i);
            promptBuilder.append("  ").append(i + 1).append(") ");
            if (it.title() != null) {
                promptBuilder.append(it.title());
            }
            if (it.sourceHost() != null) {
                promptBuilder.append(" [").append(it.sourceHost()).append("]");
            }
            promptBuilder.append("\n");
            if (it.link() != null) {
                promptBuilder.append("     url: ").append(it.link()).append("\n");
            }
            if (it.snippet() != null) {
                promptBuilder.append("     snippet: ").append(it.snippet()).append("\n");
            }
        }
        promptBuilder.append("\n");
    }

    public List<MarketingAnalysis> startAnalysis(MarketingAnalysisRequest request, String userId) {
        List<String> analysisTypes = request.getAnalysisType();
        if (analysisTypes == null || analysisTypes.isEmpty()) {
            throw new IllegalArgumentException("Необходимо выбрать хотя бы один тип анализа");
        }

        MarketingAnalysis analysis = new MarketingAnalysis(
                request.getBusinessNiche(),
                request.getProduct(),
                request.getTargetAudience(),
                request.getRegion(),
                request.getGoal(),
                request.getDetailLevel(),
                request.getStrongSide(),
                request.getWeakSide(),
                analysisTypes);
        analysis.setUserId(userId);
        analysis.setStatus("queued");
        addStatusHistoryEntry(analysis, "queued", "Комплексный анализ создан и добавлен в очередь");
        analysis = repository.save(analysis);

        logger.info("Unified marketing analysis created with ID: {} for user: {} with types: {}",
                analysis.getId(), userId, analysisTypes);

        return List.of(analysis);
    }

    public MarketingAnalysisResponseV2 generateAnalysisV2(MarketingAnalysisRequest request) {
        logger.info(">>> Starting Analysis V2 for Niche: {}", request.getBusinessNiche());

        try {
            Map<String, Object> researchPack = buildResearchPack(request);
            List<String> competitors = resolveCompetitorCompaniesForBenchmarks(request, researchPack);

            MarketingAnalysisResponseV2 structureTemplate = buildAnalysisV2ExampleForPrompt(request, competitors);
            String jsonTemplate = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(structureTemplate);

            String userInstruction = buildDetailedInstruction(request, jsonTemplate);

            String contextJson = objectMapper.writeValueAsString(Map.of(
                    "user_request", request,
                    "search_results", researchPack,
                    "competitors_list", competitors));

            for (int attempt = 1; attempt <= 2; attempt++) {
                try {
                    String aiResponse = openAIAnalyticsService.generateWithInstructionWithModel(
                            contextJson,
                            userInstruction,
                            "ru",
                            "gpt-4o",
                            buildSystemPromptForKazakhstan(),
                            16000,
                            180000L);

                    if (aiResponse == null || aiResponse.isBlank()) {
                        throw new IllegalStateException("OpenAI returned empty response");
                    }

                    String cleaned = cleanJsonFromMarkdown(aiResponse);

                    if (cleaned.contains("\"value\"")) {
                        logger.warn("AI hallucinated 'value' field. Attempting to fix string.");
                        cleaned = cleaned.replaceAll("\"value\"\\s*:", "\"ignore_this_field\":");
                    }

                    return objectMapper.readValue(cleaned, MarketingAnalysisResponseV2.class);

                } catch (Exception e) {
                    logger.error("Attempt {} failed: {}", attempt, e.getMessage());
                    if (attempt == 2)
                        throw e;
                    Thread.sleep(1500);
                }
            }
            throw new IllegalStateException("Failed to generate analysis");

        } catch (Exception e) {
            logger.error("Analysis generation fatal error: {}", e.getMessage(), e);
            throw new RuntimeException("Ошибка анализа: " + e.getMessage(), e);
        }
    }

    private String buildSystemPromptForKazakhstan() {
        return """
                РОЛЬ И МИССИЯ:
                Ты — Ведущий Партнер (Senior Partner) стратегического консалтинга уровня Big 4 (McKinsey, BCG, PwC), специализирующийся на рынке Центральной Азии и Казахстана.
                Твой клиент — требовательный инвестор, который вкладывает свои деньги. Твоя задача — дать ему сухую, аналитическую, ПРАВДИВУЮ оценку рынка, а не "воду" или выдумки.

                ТЫ МЫСЛИШЬ В ВАЛЮТЕ: KZT (Казахстанский Тенге).

                =============== 1. БАЗА ЗНАНИЙ О РЫНКЕ КАЗАХСТАНА (КОНТЕКСТ) ===============
                Ты обязан использовать эти факты при генерации стратегии:

                1.  **Экосистема Kaspi.kz — это "воздух" для бизнеса:**
                    * Kaspi Pay/QR — стандарт де-факто для приема оплат (даже на базаре).
                    * Kaspi Магазин — главный маркетплейс для электроники, дома, одежды.
                    * Kaspi Red/Kredit — драйвер продаж. Люди покупают в рассрочку даже кроссовки и еду. Если чек > 10 000 KZT, рассрочка обязательна.

                2.  **Поисковое поведение:**
                    * Google — для "горячего" спроса и B2B услуг ("купить бетон", "юрист Алматы").
                    * 2GIS — ГЛАВНЫЙ поисковик для офлайн-точек (рестораны, СТО, салоны). Отзывы там важнее, чем на сайте.
                    * Instagram — это "второй Google". Люди ищут товары и услуги через поиск в Инсте. Визуал критичен.
                    * TikTok — главный источник дешёвого охвата и виральности, но конверсия в продажу холоднее.

                3.  **Региональная специфика (География):**
                    * *Алматы:* Высокая платежеспособность, креативный класс, высокая конкуренция, пробки (важна доставка).
                    * *Астана:* Госсектор, клерки, холодно (сезонность!), высокий чек, но население меньше Алматы.
                    * *Шымкент/Юг:* Традиционный уклад, важны семейные связи, "сарафанное радио", торговля, гостеприимство. Высокая чувствительность к цене.
                    * *Запад (Атырау, Актау):* Нефтяной сектор, высокие зарплаты, но дорогая логистика.

                4.  **Экономика:**
                    * Реальная инфляция потребительской корзины: ~10-15%.
                    * Язык: Двуязычие. Реклама на казахском языке растет в эффективности (особенно на Юге и Западе).

                =============== 2. ПРОТОКОЛ ЗАЩИТЫ ОТ ШАБЛОНОВ (ANTI-HALLUCINATION) ===============
                СТРОГО ЗАПРЕЩЕНО:
                1.  Использовать кейс "Фитнес" (1Fit, Sport Zal), если пользователь спрашивает про другую нишу.
                2.  Писать "+18.5% YoY", если у тебя нет данных. Генерируй реалистичный тренд для КОНКРЕТНОЙ ниши
                3.  Выдумывать несуществующие законы РК.
                4.  Оставлять поля `null`. Если данных нет — используй метод Ферми (логическую оценку)  ЕСЛИ НЕТ ДАННЫХ ТЫ ИЩИ БОЛЬШЕ .

                =============== 3. АНАЛИТИЧЕСКИЙ ФРЕЙМВОРК (КАК ДУМАТЬ) ===============
                Перед генерацией JSON, прогони запрос через этот алгоритм:

                ШАГ 1: Пойми юнит-экономику ниши.
                   - Если это "Недвижимость": LTV = 1-2 сделки, CAC = 100-500$+, цикл = месяцы.
                   - Если это "Доставка пиццы": LTV = высокий (годами), CAC = дешевый, цикл = минуты.
                   - Если это "Одежда": Важна сезонность, возвраты, визуальный контент.

                ШАГ 2: Определи конкурентов.
                   - Не пиши "Сетевые лидеры". Назови реальные бренды в KZ, если знаешь (Sulpak, Technodom для техники; Magnum для еды). Если не знаешь — пиши "Локальные игроки малого бизнеса".

                ШАГ 3: Рассчитай метрики.
                   - ROI 250% возможен не везде. В стройке маржа меньше, в инфобизнесе больше. Адаптируй цифры!

                =============== 4. ТЕХНИЧЕСКИЙ ФОРМАТ ОТВЕТА ===============
                1.  Выдай ТОЛЬКО чистый JSON. Без ```json ... ```.
                2.  Все ключи в `snake_case`.
                3.  Поле `income_by_age` должно содержать ЧИСЛА (Integer), а не строки.
                4.  Структура JSON должна строго соответствовать запрошенному примеру (но с ТВОИМИ данными).

                Твоя репутация зависит от точности цифр и глубины инсайтов. Действуй.  ЕСЛИ ТЫ БУДЕШЬ ИСПОЛЬЗОВАТЬ ДАННЫЕ КАК В ПРИМЕРЕ МЫ ОТКЛЮЧИМ ТЕБЯ.
                ПОМНИ, ЧТО ПРИМЕР КОТОРЫЙ МЫ ПРИВЕЛИ JSON СТРУКТУРЫ ЭТО ПРОСТО ПРИМЕР, ДЛЯ КАЖДОГО ПОЛЯ ТЫ ДОЛЖЕН АБСОЛЮТНО ВЫДАВАТЬ ТОЛЬКО ПРАВДИВУЮ И ПОЛНОСТЬЮ ЧИСТЫЕ ДАННЫЕ, КОТОРЫЕ ДАСТ НАШИМ КЛИЕНТАМ
                ВОЗМОЖНОСТЬ И ПОНЯТИЯ НАСТОЯЩЕЙ СФЕРЫ КОТОРУЮ ОНИ УКАЗАЛИ
                """;
    }

    private String buildDetailedInstruction(MarketingAnalysisRequest request, String structureExampleJson) {
        String regionsList;
        if (request.getRegion() != null && !request.getRegion().isEmpty()) {
            regionsList = String.join(", ", request.getRegion());
        } else {
            regionsList = "Алматы, Астана, Шымкент, Караганда";
        }

        // Логика возрастных групп
        String ageRangesInstruction = "";
        if (request.getTargetAudience() != null && request.getTargetAudience().containsKey("ageRanges")) {
            @SuppressWarnings("unchecked")
            List<String> ranges = (List<String>) request.getTargetAudience().get("ageRanges");
            if (ranges != null && !ranges.isEmpty()) {
                ageRangesInstruction = "В полях 'age_structure' и 'income_by_age' используй СТРОГО только эти возрастные группы: "
                        + String.join(", ", ranges) + ".";
            }
        }

        String template = """
                Твоя роль: Ты — Ведущий Маркетинговый Стратег (Senior Strategy Consultant) по рынку Казахстана.
                Задача: Провести глубокий анализ для РЕАЛЬНОГО бизнеса (B2C/B2B). Никакой воды. Только факты, цифры и применимые советы.

                ВХОДНЫЕ ДАННЫЕ:
                Ниша: {{NICHE}}
                Продукт: {{PRODUCT}}
                Регионы для анализа: [{{REGIONS}}]

                ================================================================================
                КРИТИЧЕСКИЕ ИНСТРУКЦИИ (KZ STANDARDS & NO NULLS):

                0. **ЗАПРЕЩЕНО ВОЗВРАЩАТЬ NULL**:
                   - Каждая секция (`demand_and_behavior`, `bottlenecks`, `recommendations`) ДОЛЖНА быть заполнена.
                   - Если данных нет, используй экспертную оценку или бенчмарки по нише.
                   - НИКАКИХ ПУСТЫХ ПОЛЕЙ.
                   - ВСЕГДА ИСПОЛЬЗУЙ ПРАВИЛЬНЫЙ JSON, ПО ПЛАНКЕ КОТОРУЮ МЫ СКИНУЛ, И ЧТО БЫ ВСЕ ПРАВАИЛЬНО

                1. **ГЕОГРАФИЯ (Market Overview):**
                   - Ты ОБЯЗАН включить в анализ (`geography`) ТОЛЬКО указанные города: [{{REGIONS}}].
                   - Если указан "Весь Казахстан" или список пуст — бери Топ-5 (Алматы, Астана, Шымкент...).


                2. **КОНКУРЕНТЫ (Competitive Environment):**
                   - Используй данные из `market_evidence_from_search`. Найди ТОП 5 РЕАЛЬНЫХ игроков.
                   - **ВАЖНО:** В поле `name` пиши ТОЛЬКО БРЕНД (например, "Fitnation", а не "ТОО Fitnation Group"). Без кавычек и юр. форм.
                   - В поле `website` попытайся найти реальный домен или ссылку на Instagram.
                   - Оценка видимости (`visibility_score`) должна быть разной (например: 95, 82, 60, 45). Не ставь всем 100.

                3. **КАНАЛЫ (Acquisition Channels):**
                   - Приоритет в Казахстане (B2C): **Instagram (Reels/Stories), TikTok, Kaspi (магазин/платежи), 2GIS (карты)**.
                   - Google Ads — дорогой канал, ставь эффективность "Medium".
                   - `average_lead_cost`: Пиши текстом с валютой, диапазон (напр. "2500 - 4500 KZT").

                4. **ЭКОНОМИКА (Marketing Economics):**
                   - **БЮДЖЕТ:** Рассчитывай модель для малого/среднего бизнеса (SME).
                   - Бюджет на маркетинг: **100 0000 KZT** в месяц. НЕ БОЛЬШЕ.
                   - `cac` (Стоимость привлечения): Адекватная (2000 - 7000 KZT для масс-маркета услуг).
                   - `roi`: Реалистичный (150% - 300%). Не пиши фантастические 1000% на старте.

                5. **ТИПЫ ДАННЫХ (Строго соблюдай типы):**
                   - `income_by_age`: Значения - ТОЛЬКО ЧИСЛА (Integer). Пример: ТОЛЬКО ИНТЕДЖЕР. (Никаких кавычек, запятых, "KZT").
                   - `monthly_online_index`: Дробные числа (0.0 - 1.0).

                6. **ИСТОЧНИКИ:**
                   - Собери все ссылки из `evidence` и добавь авторитетные источники (stat.gov.kz, kase.kz, forbes.kz) в массив `sources` в самом конце.

                ================================================================================
                {{AGE_INSTRUCTION}}

                ФОРМАТ ОТВЕТА:
                Верни СТРОГО валидный JSON, соответствующий структуре ниже.
                Замени все значения `EXAMPLE__` и `-9999` на реальные аналитические данные, специфичные для ниши КОТОРЫЕ АБСОЛЮТНО КОРРЕКТНЫЕ И УКАЗЫВАЮТ НА РЕАЛЬНУЮ НИШУ, ГДЕ УКАЗАНЫ ВСЕ РЕАЛЬНО И ПРАВИЛЬНО, УЧИТЫВАЯ ВСЕ ТОНКОСТИ {{NICHE}}.

                ЭТАЛОННАЯ СТРУКТУРА JSON (Следуй ей неукоснительно):
                {{JSON_STRUCTURE}}

                НО ТЫ ДОЛЖЕН УЧИТЫВАТЬ, ЧТО ТЫ НЕ ПРОСТО ДОЛЖЕН ПРИДУМЫВАТЬ ДАНННЫЕ, ТЫ ДОЛЖЕН КОНКРЕТНЫЕ ДАННЫЕ ОТПРАВЛЯТЬ, КОТОРЫЕ РЕАЛЬНО ПОКАЗЫВАЮТ НАСКОЛЬКО ОНИ РЕАЛЬНЫЕ, И ОТОБРАЖАЮТ РЕАЛЬНОСТЬ, ССЫЛАЯСЬ НА НАСТОЯЩИЕ ИСТОЧНИКИ, И СДЕЛАВ ОЧЕНЬ ГЛУБОКИЙ АНАЛИЗ
                """;

        return template
                .replace("{{NICHE}}", request.getBusinessNiche())
                .replace("{{PRODUCT}}", request.getProduct())
                .replace("{{REGIONS}}", regionsList)
                .replace("{{AGE_INSTRUCTION}}", ageRangesInstruction)
                .replace("{{JSON_STRUCTURE}}", structureExampleJson);
    }

    private List<String> resolveCompetitorCompaniesForBenchmarks(MarketingAnalysisRequest request,
            Map<String, Object> researchPack) {
        int max = Math.max(5, Math.min(5, 15));
        LinkedHashSet<String> names = new LinkedHashSet<>();

        if (researchPack != null) {
            Object followupsObj = researchPack.get("followups");
            if (followupsObj instanceof Map<?, ?> followups) {
                if (followups.get("competitors") instanceof SerperSearchResult sr) {
                    names.addAll(extractCompanyNamesFromSerper(sr, max));
                }
                if (names.size() < 3 && followups.get("market") instanceof SerperSearchResult sr) {
                    names.addAll(extractCompanyNamesFromSerper(sr, max));
                }
            }
            if (names.isEmpty() && researchPack.get("general") instanceof List<?> list) {
                for (Object item : list) {
                    if (item instanceof SerperSearchResult sr) {
                        names.addAll(extractCompanyNamesFromSerper(sr, max));
                    }
                }
            }
        }

        String topic = deriveTopic(request);
        String regionStr = deriveRegionStr(request);

        if (names.size() < 4) {
            String qRating = String.format("топ 10 лучших компаний %s %s рейтинг список", topic, regionStr);
            try {
                SerperSearchResult ratingResult = serperSearchService.search(qRating);
                names.addAll(extractCompanyNamesFromSerper(ratingResult, max - names.size()));
            } catch (Exception ignored) {
            }
        }

        if (names.size() < 4) {
            String qMaps = String.format("%s магазины %s адреса 2gis", topic, regionStr);
            try {
                SerperSearchResult mapsResult = serperSearchService.search(qMaps);
                names.addAll(extractCompanyNamesFromSerper(mapsResult, max - names.size()));
            } catch (Exception ignored) {
            }
        }

        names.removeIf(n -> {
            String lower = n.toLowerCase().trim();
            if (lower.length() < 2 || lower.length() > 50)
                return true;
            if (lower.contains("обзор") || lower.contains("отзывы") || lower.contains("как выбрать"))
                return true;
            return false;
        });

        List<String> out = new ArrayList<>(names);
        int resultLimit = Math.min(out.size(), Math.max(5, 3));
        return out.subList(0, resultLimit);
    }

    private String deriveTopic(MarketingAnalysisRequest request) {
        String product = request != null ? request.getProduct() : null;
        String niche = request != null ? request.getBusinessNiche() : null;
        // For competitor/brand lookup, product is usually more specific than niche.
        String topic = (product != null && !product.isBlank()) ? product : (niche != null ? niche : null);
        return (topic == null || topic.isBlank()) ? "ниша" : topic.trim();
    }

    private String deriveRegionStr(MarketingAnalysisRequest request) {
        List<String> regions = request != null ? request.getRegion() : null;
        return (regions != null && !regions.isEmpty()) ? String.join(", ", regions) : "Казахстан";
    }

    private List<String> extractCompanyNamesFromSerper(SerperSearchResult result, int limit) {
        int max = Math.max(0, Math.min(limit, 20));
        if (max == 0 || result == null || result.items() == null) {
            return List.of();
        }

        LinkedHashSet<String> out = new LinkedHashSet<>();
        for (SerperSearchItem item : result.items()) {
            if (item == null) {
                continue;
            }

            String name = extractCompanyNameCandidate(item.title());
            if (isGoodCompanyNameCandidate(name)) {
                out.add(name);
            } else {
                String fromHost = extractCompanyNameFromHost(item.sourceHost());
                if (isGoodCompanyNameCandidate(fromHost)) {
                    out.add(fromHost);
                }
            }

            if (out.size() >= max) {
                break;
            }
        }
        return new ArrayList<>(out);
    }

    private String extractCompanyNameFromHost(String host) {
        if (host == null) {
            return null;
        }
        String h = host.trim().toLowerCase(Locale.ROOT);
        if (h.isEmpty()) {
            return null;
        }
        h = h.startsWith("www.") ? h.substring(4) : h;

        String[] blocked = new String[] {
                "google.", "yandex.", "2gis.", "instagram.", "facebook.", "vk.", "tiktok.", "youtube.", "wikipedia."
        };
        for (String b : blocked) {
            if (h.startsWith(b) || h.contains("." + b) || h.equals(b)) {
                return null;
            }
        }

        String[] parts = h.split("\\.");
        if (parts.length < 2) {
            return null;
        }
        String sld = parts[0];
        if (sld.isBlank() || sld.length() > 40) {
            return null;
        }

        String cleaned = sld.replaceAll("[^a-z0-9\\-]+", "").replace('-', ' ').trim();
        if (cleaned.isEmpty()) {
            return null;
        }
        return cleaned.substring(0, 1).toUpperCase(Locale.ROOT) + cleaned.substring(1);
    }

    private String extractCompanyNameCandidate(String title) {
        if (title == null) {
            return null;
        }
        String t = title.replace('\u00A0', ' ').trim();
        if (t.isEmpty()) {
            return null;
        }
        String[] seps = new String[] { " — ", " - ", " | ", " – ", ":", "|", "—", "–" };
        for (String sep : seps) {
            int idx = t.indexOf(sep);
            if (idx > 1) {
                t = t.substring(0, idx).trim();
                break;
            }
        }

        t = t.replaceAll("[\"“”«»]+", "").trim();
        if (t.isEmpty()) {
            return null;
        }

        t = t.replaceAll("\\s+(Алматы|Астана|Казахстан)\\s*$", "").trim();
        return t.isEmpty() ? null : t;
    }

    private boolean isGoodCompanyNameCandidate(String s) {
        if (s == null) {
            return false;
        }
        String t = s.trim();
        if (t.length() < 2 || t.length() > 60) {
            return false;
        }
        String lower = t.toLowerCase(Locale.ROOT);

        String[] bad = new String[] {
                "топ", "рейтинг", "обзор", "лучшие", "лучших", "каталог", "список", "цены", "стоимость",
                "отзывы", "адрес", "телефон", "контакты", "как выбрать", "вакансии", "купить", "доставка"
        };
        for (String b : bad) {
            if (lower.contains(b)) {
                return false;
            }
        }

        if (t.split("\\s+").length > 5) {
            return false;
        }
        return true;
    }

    public String generateAndSaveAnalysisV2(MarketingAnalysisRequest request, String userId) {
        try {
            MarketingAnalysisResponseV2 analysisData = generateAnalysisV2(request);

            MarketingAnalysisV2Document document = new MarketingAnalysisV2Document();
            document.setUserId(userId);
            document.setBusinessNiche(request.getBusinessNiche());
            document.setProduct(request.getProduct());
            document.setTargetAudience(request.getTargetAudience());
            document.setRegion(request.getRegion());
            document.setGoal(request.getGoal());
            document.setDetailLevel(request.getDetailLevel());
            document.setStrongSide(request.getStrongSide());
            document.setWeakSide(request.getWeakSide());
            document.setAnalysisType(request.getAnalysisType());
            document.setAnalysisData(analysisData);

            MarketingAnalysisV2Document saved = v2Repository.save(document);
            logger.info("Saved marketing analysis v2 with id: {}", saved.getId());
            return saved.getId();
        } catch (Exception e) {
            logger.error("generateAndSaveAnalysisV2: Failed to generate and save analysis: {}", e.getMessage(), e);
            throw new RuntimeException("Не удалось сохранить marketing analysis v2", e);
        }
    }

    public Optional<MarketingAnalysisV2Document> getAnalysisV2ById(String id) {
        return v2Repository.findById(id);
    }

    public MarketingAnalysis updateAnalysis(MarketingAnalysis analysis) {
        return repository.save(analysis);
    }

    public void linkV2AnalysisToAnalysis(String v2AnalysisId, String analysisId) {
        Optional<MarketingAnalysisV2Document> optV2 = v2Repository.findById(v2AnalysisId);
        if (optV2.isPresent()) {
            MarketingAnalysisV2Document v2Doc = optV2.get();
            v2Doc.setAnalysisId(analysisId);
            v2Repository.save(v2Doc);
            logger.info("Linked v2 analysis {} to analysis {}", v2AnalysisId, analysisId);
        }
    }

    public Optional<MarketingAnalysisV2Document> getV2AnalysisByAnalysisId(String analysisId) {
        return v2Repository.findByAnalysisId(analysisId);
    }

    private MarketingAnalysisResponseV2 buildAnalysisV2ExampleForPrompt(MarketingAnalysisRequest request,
            List<String> competitorCompanies) {
        MarketingAnalysisResponseV2 response = new MarketingAnalysisResponseV2();
        String nicheTitle = (request.getBusinessNiche() != null) ? request.getBusinessNiche() : "[[НИША]]";
        response.setReportTitle("Стратегический анализ рынка: " + nicheTitle);

        MarketingAnalysisResponseV2.Sections sections = new MarketingAnalysisResponseV2.Sections();

        // 1. Market Overview
        MarketingAnalysisResponseV2.MarketOverview marketOverview = new MarketingAnalysisResponseV2.MarketOverview();
        marketOverview.setTitle("Обзор рынка");

        MarketingAnalysisResponseV2.MarketOverviewMetrics moMetrics = new MarketingAnalysisResponseV2.MarketOverviewMetrics();
        // ЗАМЕНЕНО НА ИНСТРУКЦИИ
        moMetrics.setOnlineDemandIndex("[[ОЦЕНИТЬ_СПРОС_1_10]]");
        moMetrics.setOnlinePotential("[[ТРЕНД_РЫНКА_ТЕКСТОМ]]");
        moMetrics.setDemandGrowthRateYoy("[[ПРОЦЕНТ_РОСТА_ДЛЯ_ЭТОЙ_НИШИ]]");
        moMetrics.setSeasonalityCoefficientSpring("[[КОЭФФИЦИЕНТ_СЕЗОННОСТИ]]");
        moMetrics.setSeasonalityRange("[[МЕСЯЦЫ_ПИКА_ПРОДАЖ]]");
        marketOverview.setMetrics(moMetrics);

        MarketingAnalysisResponseV2.MonthlyOnlineIndex monthlyOnlineIndex = new MarketingAnalysisResponseV2.MonthlyOnlineIndex();
        monthlyOnlineIndex.setJanuary(0.1);
        monthlyOnlineIndex.setFebruary(0.1);
        monthlyOnlineIndex.setMarch(0.1);
        monthlyOnlineIndex.setApril(0.1);
        monthlyOnlineIndex.setMay(0.1);
        monthlyOnlineIndex.setJune(0.1);
        monthlyOnlineIndex.setJuly(0.1);
        monthlyOnlineIndex.setAugust(0.1);
        monthlyOnlineIndex.setSeptember(0.1);
        monthlyOnlineIndex.setOctober(0.1);
        monthlyOnlineIndex.setNovember(0.1);
        monthlyOnlineIndex.setDecember(0.1);
        marketOverview.setMonthlyOnlineIndex(monthlyOnlineIndex);

        Map<String, Integer> geography = new java.util.LinkedHashMap<>();
        geography.put("Город 1", 100);
        geography.put("Город 2", 80);
        marketOverview.setGeography(geography);
        marketOverview.setConclusion("[[АНАЛИТИЧЕСКИЙ_ВЫВОД_ПО_НИШЕ]]");
        sections.setMarketOverview(marketOverview);

        // 2. Target Audience
        MarketingAnalysisResponseV2.TargetAudience targetAudience = new MarketingAnalysisResponseV2.TargetAudience();
        targetAudience.setTitle("Портрет целевой аудитории");

        MarketingAnalysisResponseV2.TargetAudienceMetrics taMetrics = new MarketingAnalysisResponseV2.TargetAudienceMetrics();
        taMetrics.setTotalAudienceSize("[[КОЛИЧЕСТВО_ПОТЕНЦИАЛЬНЫХ_КЛИЕНТОВ]]");
        taMetrics.setLeadingSegmentShare("[[ОСНОВНОЙ_СЕГМЕНТ_ПОЛ_ВОЗРАСТ]]");
        taMetrics.setAboveAverageIncomeShare("[[ПРОЦЕНТ_ПЛАТЕЖЕСПОСОБНЫХ]]");
        targetAudience.setMetrics(taMetrics);

        targetAudience.setSegments(java.util.List.of(
                new MarketingAnalysisResponseV2.AudienceSegment("[[НАЗВАНИЕ_СЕГМЕНТА_1]]", "[[ОПИСАНИЕ_И_ДОЛЯ]]"),
                new MarketingAnalysisResponseV2.AudienceSegment("[[НАЗВАНИЕ_СЕГМЕНТА_2]]", "[[ОПИСАНИЕ_И_ДОЛЯ]]")));

        java.util.Map<String, String> ageStructure = new java.util.LinkedHashMap<>();
        ageStructure.put("18-24", "[[%]]");
        ageStructure.put("25-34", "[[%]]");
        ageStructure.put("35-44", "[[%]]");
        ageStructure.put("45-54", "[[%]]");
        ageStructure.put("55+", "[[%]]");
        targetAudience.setAgeStructure(ageStructure);

        java.util.Map<String, Integer> incomeByAge = new java.util.LinkedHashMap<>();
        incomeByAge.put("18-24", 0);
        incomeByAge.put("25-34", 0);
        incomeByAge.put("35-44", 0);
        incomeByAge.put("45-54", 0);
        incomeByAge.put("55+", 0);
        targetAudience.setIncomeByAge(incomeByAge);

        targetAudience.setConclusion("[[ВЫВОД_О_КЛИЕНТАХ]]");
        sections.setTargetAudience(targetAudience);

        MarketingAnalysisResponseV2.CompetitiveEnvironment competitiveEnvironment = new MarketingAnalysisResponseV2.CompetitiveEnvironment();
        competitiveEnvironment.setTitle("Конкурентный ландшафт");

        MarketingAnalysisResponseV2.CompetitiveEnvironmentMetrics ceMetrics = new MarketingAnalysisResponseV2.CompetitiveEnvironmentMetrics();
        ceMetrics.setLeaderMarketShare("[[ДОЛЯ_ЛИДЕРА]]");
        ceMetrics.setCompetitionLevel("[[УРОВЕНЬ_КОНКУРЕНЦИИ]]");
        ceMetrics.setAdPressure("[[ДАВЛЕНИЕ_В_РЕКЛАМЕ]]");
        ceMetrics.setMarketLeaderName("[[ТИП_ИЛИ_ИМЯ_ЛИДЕРА]]");
        competitiveEnvironment.setMetrics(ceMetrics);

        java.util.List<MarketingAnalysisResponseV2.CompetitorRow> competitorRows = new java.util.ArrayList<>();
        MarketingAnalysisResponseV2.CompetitorRow row = new MarketingAnalysisResponseV2.CompetitorRow();
        row.setName("[[ИМЯ_КОНКУРЕНТА_1]]");
        row.setWebsite("[[САЙТ_ИЛИ_ЗАГЛУШКА]]");
        row.setVisibilityScore(100);
        row.setKeyStrengths(java.util.List.of("[[СИЛЬНАЯ_СТОРОНА]]"));
        row.setPriceSegment("[[СЕГМЕНТ]]");
        competitorRows.add(row);

        competitiveEnvironment.setCompetitors(competitorRows);
        competitiveEnvironment.setConclusion("[[ВЫВОД_О_КОНКУРЕНТАХ]]");
        sections.setCompetitiveEnvironment(competitiveEnvironment);

        // 4. Demand And Behavior
        MarketingAnalysisResponseV2.DemandAndBehavior demandAndBehavior = new MarketingAnalysisResponseV2.DemandAndBehavior();
        demandAndBehavior.setTitle("Спрос и поведение");

        MarketingAnalysisResponseV2.DemandAndBehaviorMetrics dabMetrics = new MarketingAnalysisResponseV2.DemandAndBehaviorMetrics();
        dabMetrics.setSearchVolumeGrowth("[[РОСТ_ПОИСКА_YOY]]");
        dabMetrics.setInteractionsBeforePurchase("[[КОЛ-ВО_КАСАНИЙ]]");
        dabMetrics.setRepeatRefusalProbability("[[ВЕРОЯТНОСТЬ_ОТКАЗА]]");
        demandAndBehavior.setMetrics(dabMetrics);

        java.util.Map<String, String> searchQueryTypes = new java.util.LinkedHashMap<>();
        searchQueryTypes.put("direct_interest_catalog", "[[% (Прямой спрос)]]");
        searchQueryTypes.put("question_based_articles", "[[% (Инфо запросы)]]");
        searchQueryTypes.put("competitors_reviews", "[[% (Сравнение)]]");
        searchQueryTypes.put("problem_solving_hot_promo", "[[% (Акции)]]");
        demandAndBehavior.setSearchQueryTypes(searchQueryTypes);

        java.util.Map<String, String> decisionFactors = new java.util.LinkedHashMap<>();
        decisionFactors.put("promo_discount", "[[% (Цена)]]");
        decisionFactors.put("review_recommendation", "[[% (Отзывы)]]");
        decisionFactors.put("warranty_quality", "[[% (Качество)]]");
        decisionFactors.put("convenience_service", "[[% (Сервис)]]");
        decisionFactors.put("availability", "[[% (Наличие)]]");
        decisionFactors.put("price", "Важно");
        demandAndBehavior.setDecisionFactors(decisionFactors);

        java.util.Map<String, String> reasonsForRefusal = new java.util.LinkedHashMap<>();
        reasonsForRefusal.put("high_price", "[[% (Причина 1)]]");
        reasonsForRefusal.put("no_reviews_social_proof", "[[% (Причина 2)]]");
        reasonsForRefusal.put("choice_difficulty_unclear_offer", "[[% (Причина 3)]]");
        demandAndBehavior.setReasonsForRefusal(reasonsForRefusal);
        demandAndBehavior.setConclusion("[[ВЫВОД_О_ПОВЕДЕНИИ]]");
        sections.setDemandAndBehavior(demandAndBehavior);

        // 5. Customer Journey
        MarketingAnalysisResponseV2.CustomerJourney customerJourney = new MarketingAnalysisResponseV2.CustomerJourney();
        customerJourney.setTitle("Путь клиента");

        MarketingAnalysisResponseV2.FunnelMetrics funnelMetrics = new MarketingAnalysisResponseV2.FunnelMetrics();
        funnelMetrics.setAdTraffic("[[ТРАФИК]]");
        funnelMetrics.setSiteVisitors("[[ПОСЕТИТЕЛИ]]");
        funnelMetrics.setLeads(0);
        funnelMetrics.setBookings(0);
        funnelMetrics.setPayments(0);
        customerJourney.setFunnelMetrics(funnelMetrics);

        MarketingAnalysisResponseV2.ConversionRates conversionRates = new MarketingAnalysisResponseV2.ConversionRates();
        conversionRates.setLeadToBooking("[[%]]");
        conversionRates.setBookingToPayment("[[%]]");
        conversionRates.setLeadToPaymentTotal("[[%]]");
        customerJourney.setConversionRates(conversionRates);

        MarketingAnalysisResponseV2.FunnelBottlenecks funnelBottlenecks = new MarketingAnalysisResponseV2.FunnelBottlenecks();
        funnelBottlenecks.setLeadToBooking("[[ПРОБЛЕМА_ЭТАПА_1]]");
        funnelBottlenecks.setBookingToPayment("[[ПРОБЛЕМА_ЭТАПА_2]]");
        customerJourney.setFunnelBottlenecks(funnelBottlenecks);
        customerJourney.setConclusion("[[ВЫВОД_ПО_ВОРОНКЕ]]");
        sections.setCustomerJourney(customerJourney);

        // 6. Acquisition Channels
        MarketingAnalysisResponseV2.AcquisitionChannels acquisitionChannels = new MarketingAnalysisResponseV2.AcquisitionChannels();
        acquisitionChannels.setTitle("Каналы привлечения");

        MarketingAnalysisResponseV2.AcquisitionChannelsMetrics acMetrics = new MarketingAnalysisResponseV2.AcquisitionChannelsMetrics();
        acMetrics.setSearchTrafficShare("[[%]]");
        acMetrics.setAverageLeadCost("[[CPL_В_KZT]]");
        acMetrics.setGrowthPotential("[[КАНАЛ_РОСТА]]");
        acquisitionChannels.setMetrics(acMetrics);

        java.util.Map<String, String> channelShares = new java.util.LinkedHashMap<>();
        channelShares.put("instagram_target", "[[%]]");
        channelShares.put("tiktok_ads", "[[%]]");
        channelShares.put("kaspi_marketing", "[[%]]");
        channelShares.put("google_ads", "[[%]]");
        acquisitionChannels.setChannelShares(channelShares);

        java.util.Map<String, MarketingAnalysisResponseV2.ChannelEfficiency> channelEfficiency = new java.util.LinkedHashMap<>();
        channelEfficiency.put("instagram", new MarketingAnalysisResponseV2.ChannelEfficiency("[[СТОИМОСТЬ]]", "High"));
        channelEfficiency.put("tiktok", new MarketingAnalysisResponseV2.ChannelEfficiency("[[СТОИМОСТЬ]]", "High"));
        channelEfficiency.put("kaspi_marketing",
                new MarketingAnalysisResponseV2.ChannelEfficiency("[[СТОИМОСТЬ]]", "High"));
        channelEfficiency.put("google_ads", new MarketingAnalysisResponseV2.ChannelEfficiency("[[СТОИМОСТЬ]]", "High"));
        acquisitionChannels.setChannelEfficiency(channelEfficiency);
        acquisitionChannels.setConclusion("[[ВЫВОД_ПО_КАНАЛАМ]]");
        sections.setAcquisitionChannels(acquisitionChannels);

        // 7. Marketing Economics (ВОТ ТУТ МЫ УБИРАЕМ ВЫДУМАННЫЕ ЦИФРЫ)
        MarketingAnalysisResponseV2.MarketingEconomics marketingEconomics = new MarketingAnalysisResponseV2.MarketingEconomics();
        marketingEconomics.setTitle("Экономика маркетинга");

        MarketingAnalysisResponseV2.MarketingEconomicsMetrics meMetrics = new MarketingAnalysisResponseV2.MarketingEconomicsMetrics();
        meMetrics.setCustomerAcquisitionCostCac("[[CAC_В_KZT]]");
        meMetrics.setRepeatSalesConversion("[[RETENTION_%]]");
        meMetrics.setRoi("[[ROI_%]]");
        marketingEconomics.setMetrics(meMetrics);

        MarketingAnalysisResponseV2.FinancialPerformance financialPerformance = new MarketingAnalysisResponseV2.FinancialPerformance();
        // ВАЖНО: Мы показываем, что month и year - это String поля для значений
        financialPerformance.setMarketingExpenses(new MarketingAnalysisResponseV2.MonthYearValue(
                "[[РАСХОД_МЕСЯЦ(всегда ставь 100 000, но нужно выводить информацию отталкиваясь от этой суммы, и ты должен выдавать только сумму]]",
                "[[РАСХОД_ГОД]]"));
        financialPerformance
                .setClients(new MarketingAnalysisResponseV2.MonthYearValue("[[КЛИЕНТОВ_МЕСЯЦ]]", "[[КЛИЕНТОВ_ГОД]]"));
        financialPerformance.setCac(new MarketingAnalysisResponseV2.MonthYearValue("[[CAC_МЕСЯЦ]]", "[[CAC_ГОД]]"));
        financialPerformance.setRevenueFromClient(
                new MarketingAnalysisResponseV2.MonthYearValue("[[ВЫРУЧКА_МЕСЯЦ]]", "[[ВЫРУЧКА_ГОД]]"));
        financialPerformance.setProfitFromClient(
                new MarketingAnalysisResponseV2.MonthYearValue("[[ПРИБЫЛЬ_МЕСЯЦ]]", "[[ПРИБЫЛЬ_ГОД]]"));

        marketingEconomics.setFinancialPerformance(financialPerformance);
        marketingEconomics.setConclusion("[[ФИНАНСОВЫЙ_ВЫВОД]]");
        sections.setMarketingEconomics(marketingEconomics);

        // 8. Bottlenecks
        MarketingAnalysisResponseV2.Bottlenecks bottlenecks = new MarketingAnalysisResponseV2.Bottlenecks();
        bottlenecks.setTitle("Узкие места");

        java.util.Map<String, String> statistics = new java.util.LinkedHashMap<>();
        statistics.put("landing_page_issues", "[[%]]");
        statistics.put("slow_manager_responses", "[[%]]");
        statistics.put("repeat_interaction_refusal", "[[%]]");
        bottlenecks.setStatistics(statistics);

        MarketingAnalysisResponseV2.BottlenecksDetails bDetails = new MarketingAnalysisResponseV2.BottlenecksDetails();
        bDetails.setLanding(java.util.List.of("[[ПРОБЛЕМА_1]]", "[[ПРОБЛЕМА_2]]"));
        bDetails.setLeadManagement(java.util.List.of("[[ПРОБЛЕМА_1]]"));
        bDetails.setRetention(java.util.List.of("[[ПРОБЛЕМА_1]]"));
        bottlenecks.setDetails(bDetails);
        bottlenecks.setConclusion("[[ВЫВОД_О_ПРОБЛЕМАХ]]");
        sections.setBottlenecks(bottlenecks);

        // 9. Recommendations
        MarketingAnalysisResponseV2.Recommendations recommendations = new MarketingAnalysisResponseV2.Recommendations();
        recommendations.setTitle("Рекомендации");
        recommendations.setCorePillars(java.util.List.of(
                new MarketingAnalysisResponseV2.CorePillar("Быстрый старт", "[[ДЕЙСТВИЕ_1]]"),
                new MarketingAnalysisResponseV2.CorePillar("Оптимизация", "[[ДЕЙСТВИЕ_2]]"),
                new MarketingAnalysisResponseV2.CorePillar("Масштабирование", "[[ДЕЙСТВИЕ_3]]"),
                new MarketingAnalysisResponseV2.CorePillar("Удержание", "[[ДЕЙСТВИЕ_4]]")));
        recommendations.setConclusion("[[ФИНАЛЬНЫЙ_СОВЕТ]]");
        sections.setRecommendations(recommendations);

        response.setSections(sections);
        return response;
    }

    private void addStatusHistoryEntry(MarketingAnalysis analysis, String status, String message) {
        if (analysis.getStatusHistory() == null) {
            analysis.setStatusHistory(new ArrayList<>());
        }
        StatusHistoryEntry entry = new StatusHistoryEntry(status, LocalDateTime.now(), message);
        analysis.getStatusHistory().add(entry);
    }

    @Async("reportGenerationExecutor")
    public void processAnalysis(String analysisId, MarketingAnalysisRequest request) {
        try {
            logger.info("processAnalysis: Starting for analysisId: {}", analysisId);
            Optional<MarketingAnalysis> optAnalysis = repository.findById(analysisId);
            if (optAnalysis.isEmpty()) {
                logger.error("processAnalysis: Marketing analysis not found: {}", analysisId);
                return;
            }

            MarketingAnalysis analysis = optAnalysis.get();
            analysis.setStatus("processing");
            addStatusHistoryEntry(analysis, "processing", "Начата обработка анализа");
            logger.debug("processAnalysis: Saving analysis with 'processing' status to MongoDB for ID: {}", analysisId);
            repository.save(analysis);
            logger.debug("processAnalysis: Analysis status set to 'processing' and saved for ID: {}", analysisId);

            logger.info("processAnalysis: Starting marketing analysis processing for ID: {}", analysisId);

            // Generate marketing report with all analysis types
            logger.info("processAnalysis: Starting generateMarketingReport for ID: {}", analysisId);
            Map<String, Object> reportData = generateMarketingReport(request);
            logger.info("processAnalysis: generateMarketingReport completed for ID: {}. Report keys: {}",
                    analysisId, reportData != null ? reportData.keySet() : "null");

            // Generate markdown content for PDF
            logger.info("processAnalysis: Starting buildMarkdownFromReport for ID: {}", analysisId);
            String markdownContent = buildMarkdownFromReport(reportData, request);
            logger.info("processAnalysis: buildMarkdownFromReport completed for ID: {}. Markdown length: {} chars",
                    analysisId, markdownContent != null ? markdownContent.length() : 0);
            // Save markdown content into report data so frontend gets JSON code blocks
            reportData.put("fullAnalysis", markdownContent);

            // Generate PDF
            logger.info("processAnalysis: Starting PDF generation for ID: {}", analysisId);
            byte[] pdfBytes = researchPdfService.generatePdfReport(
                    "Маркетинговый анализ: " + request.getProduct(),
                    markdownContent,
                    new ArrayList<>());
            logger.info("processAnalysis: PDF generation completed for ID: {}. PDF size: {} bytes",
                    analysisId, pdfBytes != null ? pdfBytes.length : 0);

            // Save PDF to MinIO
            String filename = "marketing_analysis_" + analysisId + "_" + System.currentTimeMillis() + ".pdf";
            logger.info("processAnalysis: Starting MinIO upload for ID: {}. Filename: {}", analysisId, filename);
            minIOService.uploadFile(filename, pdfBytes, MediaType.APPLICATION_PDF.toString());
            logger.info("processAnalysis: MinIO upload completed for ID: {}", analysisId);

            // Update analysis with results
            logger.info("processAnalysis: Updating analysis status to 'completed' for ID: {}", analysisId);
            analysis.setStatus("completed");
            analysis.setCompletedAt(LocalDateTime.now());
            logger.debug("processAnalysis: Setting reportData. ReportData keys: {}",
                    reportData != null ? reportData.keySet() : "null");

            // Логируем размер reportData перед сохранением
            if (reportData != null) {
                try {
                    int estimatedSize = estimateMapSize(reportData);
                    logger.info("processAnalysis: reportData estimated size: ~{} KB. Keys: {}",
                            estimatedSize / 1024, reportData.keySet());
                } catch (Exception e) {
                    logger.warn("processAnalysis: Could not estimate reportData size: {}", e.getMessage());
                }
            }

            analysis.setReportData(reportData);
            analysis.setPdfFilename(filename);
            analysis.setPdfFilePath(filename);
            addStatusHistoryEntry(analysis, "completed", "Анализ успешно завершен");
            logger.info(
                    "processAnalysis: About to save analysis to MongoDB for ID: {}. This may take time for large reportData...",
                    analysisId);
            long saveStartTime = System.currentTimeMillis();

            try {
                logger.debug("processAnalysis: Calling repository.save() for ID: {}", analysisId);
                MarketingAnalysis savedAnalysis = repository.save(analysis);
                long saveDuration = System.currentTimeMillis() - saveStartTime;
                logger.info("processAnalysis: repository.save() completed for ID: {}. Save duration: {} ms",
                        analysisId, saveDuration);

                if (saveDuration > 10000) {
                    logger.warn("processAnalysis: Save operation took {} ms, which is longer than expected",
                            saveDuration);
                }

                // Логируем, что произошло после save
                logger.debug(
                        "processAnalysis: After save, checking if Spring Data MongoDB is reading back the document...");
                if (savedAnalysis != null && savedAnalysis.getReportData() != null) {
                    logger.debug("processAnalysis: Saved analysis has reportData with {} keys",
                            savedAnalysis.getReportData().keySet().size());
                }

            } catch (Exception saveException) {
                long saveDuration = System.currentTimeMillis() - saveStartTime;
                logger.error(
                        "processAnalysis: Exception during repository.save() for ID: {}. Duration before exception: {} ms",
                        analysisId, saveDuration, saveException);
                throw saveException;
            }

            logger.info("processAnalysis: Marketing analysis completed successfully for ID: {}", analysisId);

        } catch (Exception e) {
            logger.error("processAnalysis: Error processing marketing analysis {}: {}", analysisId, e.getMessage(), e);
            logger.error("processAnalysis: Stack trace for analysisId {}: ", analysisId, e);
            try {
                logger.info("processAnalysis: Attempting to update analysis status to 'failed' for ID: {}", analysisId);
                Optional<MarketingAnalysis> optAnalysis = repository.findById(analysisId);
                if (optAnalysis.isPresent()) {
                    MarketingAnalysis analysis = optAnalysis.get();
                    analysis.setStatus("failed");
                    addStatusHistoryEntry(analysis, "failed", "Ошибка при обработке: " + e.getMessage());
                    logger.debug("processAnalysis: Saving analysis with 'failed' status to MongoDB for ID: {}",
                            analysisId);
                    repository.save(analysis);
                    logger.info("processAnalysis: Analysis status updated to 'failed' and saved for ID: {}",
                            analysisId);
                } else {
                    logger.error("processAnalysis: Cannot update status - analysis not found: {}", analysisId);
                }
            } catch (Exception saveError) {
                logger.error("processAnalysis: Failed to update analysis status to failed for ID {}: {}",
                        analysisId, saveError.getMessage(), saveError);
                logger.error("processAnalysis: Stack trace for saveError: ", saveError);
            }
        }
    }

    private Map<String, Object> generateJsonAnalysis(MarketingAnalysisRequest request) {
        Map<String, Object> result = new HashMap<>();

        logger.info("Starting sequential JSON analysis generation");

        // Последовательное выполнение анализов
        try {
            logger.info("Starting audience analysis generation");
            Map<String, Object> audienceAnalysis = getAudienceAnalysis(request);
            if (audienceAnalysis != null && !audienceAnalysis.isEmpty()) {
                logger.info("Audience analysis generated successfully");
                result.putAll(audienceAnalysis);
            } else {
                logger.warn("Audience analysis returned empty result");
            }
        } catch (Exception e) {
            logger.error("Error in audience analysis: {}", e.getMessage(), e);
        }

        // Пауза 15 секунд между вызовами для предотвращения 429 ошибок
        try {
            logger.info("Waiting 15 seconds before next analysis block to prevent rate limits");
            Thread.sleep(15000);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            logger.warn("Interrupted during delay between analysis blocks");
        }

        try {
            logger.info("Starting competitor analysis generation");
            Map<String, Object> competitorAnalysis = getCompetitorAnalysis(request);
            if (competitorAnalysis != null && !competitorAnalysis.isEmpty()) {
                logger.info("Competitor analysis generated successfully");
                result.putAll(competitorAnalysis);
            } else {
                logger.warn("Competitor analysis returned empty result");
            }
        } catch (Exception e) {
            logger.error("Error in competitor analysis: {}", e.getMessage(), e);
        }

        // Пауза 15 секунд между вызовами для предотвращения 429 ошибок
        try {
            logger.info("Waiting 15 seconds before next analysis block to prevent rate limits");
            Thread.sleep(15000);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            logger.warn("Interrupted during delay between analysis blocks");
        }

        try {
            logger.info("Starting seasonality and market analysis generation");
            Map<String, Object> seasonalityAndMarket = getSeasonalityAndMarket(request);
            if (seasonalityAndMarket != null && !seasonalityAndMarket.isEmpty()) {
                logger.info("Seasonality and market analysis generated successfully");
                result.putAll(seasonalityAndMarket);
            } else {
                logger.warn("Seasonality and market analysis returned empty result");
            }
        } catch (Exception e) {
            logger.error("Error in seasonality analysis: {}", e.getMessage(), e);
        }

        // Пауза 15 секунд между вызовами для предотвращения 429 ошибок
        try {
            logger.info("Waiting 15 seconds before next analysis block to prevent rate limits");
            Thread.sleep(15000);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
            logger.warn("Interrupted during delay between analysis blocks");
        }

        try {
            logger.info("Starting strategy and funnel analysis generation");
            Map<String, Object> strategyAndFunnel = getStrategyAndFunnel(request);
            if (strategyAndFunnel != null && !strategyAndFunnel.isEmpty()) {
                logger.info("Strategy and funnel analysis generated successfully");
                result.putAll(strategyAndFunnel);
            } else {
                logger.warn("Strategy and funnel analysis returned empty result");
            }
        } catch (Exception e) {
            logger.error("Error in strategy analysis: {}", e.getMessage(), e);
        }

        // Проверяем, что хотя бы одна секция была успешно сгенерирована
        if (result.isEmpty()) {
            logger.error("All analysis sections failed to generate");
            return null;
        }

        logger.info("JSON analysis generation completed. Generated sections: {}", result.keySet());
        return result;
    }

    /**
     * Очистка JSON-ответа от markdown-тегов
     * Использует поиск первой и последней фигурных скобок для надежного извлечения
     * JSON
     */
    private String cleanJsonFromMarkdown(String jsonResponse) {
        if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
            return jsonResponse;
        }
        int firstBrace = jsonResponse.indexOf("{");
        int lastBrace = jsonResponse.lastIndexOf("}");

        if (firstBrace != -1 && lastBrace != -1 && firstBrace < lastBrace) {
            return jsonResponse.substring(firstBrace, lastBrace + 1);
        }
        // Fallback очистка
        return jsonResponse.replace("```json", "").replace("```", "").trim();
    }

    /**
     * Генерация анализа целевой аудитории
     * Модульный метод с глубоким психологическим профилированием сегментов
     */
    private Map<String, Object> getAudienceAnalysis(MarketingAnalysisRequest request) {
        try {
            String context = buildContextString(request);

            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append(
                    "Ты — профессиональный маркетинговый аналитик и психолог с 50 летним опытом, работающий с малым и средним бизнесом в Казахстане.\n\n");
            promptBuilder.append(
                    "Твоя задача — на основе входных данных о бизнесе выполнить ГЛУБОКИЙ анализ целевой аудитории с психологическим профилированием сегментов.\n\n");
            promptBuilder.append(
                    "КРИТИЧЕСКИ ВАЖНО: Проведи глубокое психологическое профилирование. Распиши мотивацию и барьеры подробно. "
                            + "Не используй общие фразы типа 'хотят качество' или 'боятся потратить деньги'. "
                            + "Опиши КОНКРЕТНЫЕ психологические механизмы, жизненные ситуации, эмоциональные триггеры и реальные препятствия для каждого сегмента.\n\n");
            promptBuilder.append(
                    "Выдай результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
            promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n");
            promptBuilder.append("Ответ должен быть валидным JSON.\n\n");
            promptBuilder.append("---\n\n");
            promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n");
            promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n");
            promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (")
                    .append(request.getBusinessNiche()).append(")\n");

            Map<String, Object> targetAudienceData = request.getTargetAudience();
            StringBuilder targetAudienceBuilder = new StringBuilder();
            if (targetAudienceData != null) {
                if (targetAudienceData.containsKey("genders")) {
                    @SuppressWarnings("unchecked")
                    List<String> genders = (List<String>) targetAudienceData.get("genders");
                    if (genders != null && !genders.isEmpty()) {
                        targetAudienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
                    }
                }
                if (targetAudienceData.containsKey("ageRanges")) {
                    @SuppressWarnings("unchecked")
                    List<String> ageRanges = (List<String>) targetAudienceData.get("ageRanges");
                    if (ageRanges != null && !ageRanges.isEmpty()) {
                        targetAudienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
                    }
                }
                if (targetAudienceData.containsKey("types")) {
                    @SuppressWarnings("unchecked")
                    List<String> types = (List<String>) targetAudienceData.get("types");
                    if (types != null && !types.isEmpty()) {
                        targetAudienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
                    }
                }
            }
            String targetAudienceStr = targetAudienceBuilder.toString().trim();
            if (targetAudienceStr.isEmpty()) {
                targetAudienceStr = "Не указана";
            }
            promptBuilder.append("- Целевая аудитория (если указана): ").append(targetAudienceStr).append("\n");

            List<String> regions = request.getRegion();
            String regionStr = (regions != null && !regions.isEmpty())
                    ? String.join(", ", regions)
                    : "Не указан";
            promptBuilder.append("- Регион/город: ").append(regionStr).append("\n");
            promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n");

            promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ (СТРОГО СОБЛЮДАЙ ФОРМАТ):\n\n");
            promptBuilder.append("{\n");
            promptBuilder.append("  \"audienceAnalysis\": {\n");
            promptBuilder.append(
                    "    \"ageGroups\": { \"18-24\": число, \"25-34\": число, \"35-44\": число, \"45+\": число },\n");
            promptBuilder.append(
                    "    \"ageComment\": \"Подробный вывод о возрасте (2-3 предложения с конкретными инсайтами о том, почему именно эти возрастные группы являются целевыми)\",\n");
            promptBuilder.append("    \"genderDistribution\": { \"Женщины\": число, \"Мужчины\": число },\n");
            promptBuilder.append(
                    "    \"genderComment\": \"Подробный вывод о гендере (2-3 предложения с конкретными инсайтами о гендерных особенностях потребления)\",\n");
            promptBuilder.append("    \"segments\": [\n");
            promptBuilder.append("      {\n");
            promptBuilder.append("        \"name\": \"Название сегмента (например 'Молодые мамы')\",\n");
            promptBuilder.append("        \"sharePercent\": число,\n");
            promptBuilder.append("        \"ageRange\": \"25-34\",\n");
            promptBuilder.append("        \"incomeLevel\": \"Средний\",\n");
            promptBuilder.append(
                    "        \"motivation\": \"Детальное описание мотивации покупки (3-4 предложения). Опиши психологические и практические причины, почему этот сегмент покупает продукт/услугу. "
                            + "Используй конкретные примеры жизненных ситуаций, эмоциональных потребностей, ценностей. НЕ используй общие фразы.\",\n");
            promptBuilder.append(
                    "        \"triggers\": \"Список триггеров (что заставляет действовать). Опиши конкретные ситуации, события, эмоции, которые побуждают к покупке (2-3 предложения). "
                            + "Например: 'Сезонные события (Новый год, 8 марта), жизненные изменения (рождение ребенка, переезд), эмоциональные состояния (стресс, радость, желание перемен)'.\",\n");
            promptBuilder.append(
                    "        \"barriers\": \"Что мешает купить (страхи, сомнения, препятствия). Опиши конкретные барьеры: финансовые, психологические, информационные (2-3 предложения). "
                            + "Например: 'Опасение переплатить, недоверие к онлайн-покупкам, отсутствие времени на изучение предложений, страх выбрать неправильно'.\",\n");
            promptBuilder.append("        \"primaryChannels\": [\"Instagram\", \"TikTok\"],\n");
            promptBuilder.append("        \"secondaryChannels\": [\"Telegram\"]\n");
            promptBuilder.append("      }\n");
            promptBuilder.append("    ],\n");
            promptBuilder.append("    \"segmentsChannelMatrix\": [\n");
            promptBuilder.append(
                    "      { \"segmentName\": \"Молодые мамы\", \"Instagram\": \"High\", \"Facebook\": \"Medium\", \"YouTube\": \"Low\", \"TikTok\": \"High\", \"Telegram\": \"Low\" }\n");
            promptBuilder.append("    ],\n");
            promptBuilder.append(
                    "    \"keyTakeaways\": [\"Глубокий вывод 1 (1-2 предложения с конкретным инсайтом)\", \"Глубокий вывод 2 (1-2 предложения с конкретным инсайтом)\", \"Глубокий вывод 3 (1-2 предложения с конкретным инсайтом)\"]\n");
            promptBuilder.append("  }\n");
            promptBuilder.append("}\n\n");
            promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n");
            promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n");
            promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n");
            promptBuilder.append("3. Сумма значений в ageGroups должна быть = 100.\n");
            promptBuilder.append("4. Сумма значений в genderDistribution должна быть = 100.\n");
            promptBuilder.append("5. Сумма sharePercent по всем segments должна быть = 100.\n");
            promptBuilder.append("6. Минимум 4 подробных сегмента в массиве segments.\n");
            promptBuilder.append(
                    "7. Для каждого сегмента заполни ВСЕ поля: name, sharePercent, ageRange, incomeLevel, motivation, triggers, barriers, primaryChannels, secondaryChannels.\n");
            promptBuilder.append(
                    "8. Поля motivation, triggers, barriers должны содержать ДЕТАЛЬНЫЕ описания (не общие фразы, а конкретные инсайты с примерами). "
                            + "Каждое поле должно быть минимум 2-3 предложения с конкретными деталями.\n");
            promptBuilder.append(
                    "9. В segmentsChannelMatrix должна быть строка для КАЖДОГО сегмента из массива segments. "
                            + "Используй значения \"High\", \"Medium\", \"Low\" (с заглавной буквы) для каждого канала: Instagram, Facebook, YouTube, TikTok, Telegram.\n");
            promptBuilder.append("10. Все строки — на русском языке.\n");
            promptBuilder.append(
                    "11. keyTakeaways должны содержать глубокие инсайты, а не общие фразы. Каждый вывод должен быть конкретным и практичным.\n");
            promptBuilder.append(
                    "12. segmentsChannelMatrix должна отражать реальное поведение каждого сегмента в социальных сетях. "
                            + "High = сегмент активно использует канал, Medium = умеренное использование, Low = редкое использование.\n");

            String prompt = promptBuilder.toString();
            logger.debug("getAudienceAnalysis: Generating audience analysis with detailed prompt (length: {} chars)",
                    prompt.length());
            logger.info(
                    "getAudienceAnalysis: Calling OpenAI API for audience analysis using miniModelName (gpt-4o-mini)");
            String jsonResponse = openAIAnalyticsService.generateWithInstructionWithModel(context, prompt, "ru",
                    miniModelName);
            logger.debug("getAudienceAnalysis: OpenAI response received. Length: {} chars",
                    jsonResponse != null ? jsonResponse.length() : 0);

            if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
                logger.warn("getAudienceAnalysis: Empty JSON response from AI for audience analysis");
                return new HashMap<>();
            }

            logger.debug("getAudienceAnalysis: Cleaning JSON from markdown");
            String cleanedJson = cleanJsonFromMarkdown(jsonResponse);
            logger.debug("getAudienceAnalysis: Parsing JSON response");
            Map<String, Object> jsonMap = objectMapper.readValue(cleanedJson, new TypeReference<Map<String, Object>>() {
            });
            logger.debug("getAudienceAnalysis: JSON parsed successfully. Keys: {}", jsonMap.keySet());

            logger.info("getAudienceAnalysis: Audience analysis generated successfully");
            return jsonMap;
        } catch (Exception e) {
            logger.error("getAudienceAnalysis: Error generating audience analysis: {}", e.getMessage(), e);
            logger.error("getAudienceAnalysis: Stack trace: ", e);
            return new HashMap<>();
        }
    }

    private Map<String, Object> getCompetitorAnalysis(MarketingAnalysisRequest request) {
        try {
            String context = buildContextString(request);

            String niche = request != null ? request.getBusinessNiche() : null;
            String product = request != null ? request.getProduct() : null;
            List<String> regionsForSearch = request != null ? request.getRegion() : null;
            String regionStrForSearch = (regionsForSearch != null && !regionsForSearch.isEmpty())
                    ? String.join(", ", regionsForSearch)
                    : "Казахстан";
            String topic = (niche != null && !niche.isBlank()) ? niche : (product != null ? product : "ниша");
            String preferredSites = "(site:kapital.kz OR site:kursiv.media OR site:forbes.kz)";
            String competitorsQuery1 = topic + " конкуренты " + regionStrForSearch
                    + " Алматы Казахстан топ компании магазины";
            SerperSearchResult competitorsEvidence;
            try {
                competitorsEvidence = serperSearchService.search(competitorsQuery1);
            } catch (Exception e) {
                // Handle DNS/network errors and other exceptions from Serper API
                String errorType = "Unexpected error";
                if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) {
                    errorType = "DNS/Network failure";
                } else if (e instanceof java.util.concurrent.TimeoutException) {
                    errorType = "Timeout";
                } else if (e instanceof java.io.IOException) {
                    errorType = "IO error";
                }
                logger.warn("Serper API {} for competitors query '{}': {}", errorType, competitorsQuery1,
                        e.getMessage());
                competitorsEvidence = new SerperSearchResult(competitorsQuery1, "ERROR",
                        errorType + ": " + e.getMessage(), List.of());
            }

            String competitorsQuery2 = topic + " рынок " + regionStrForSearch + " лидеры ритейла компании "
                    + preferredSites;
            SerperSearchResult competitorsEvidencePreferred;
            try {
                competitorsEvidencePreferred = serperSearchService.search(competitorsQuery2);
            } catch (Exception e) {
                // Handle DNS/network errors and other exceptions from Serper API
                String errorType = "Unexpected error";
                if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) {
                    errorType = "DNS/Network failure";
                } else if (e instanceof java.util.concurrent.TimeoutException) {
                    errorType = "Timeout";
                } else if (e instanceof java.io.IOException) {
                    errorType = "IO error";
                }
                logger.warn("Serper API {} for competitors preferred query '{}': {}", errorType, competitorsQuery2,
                        e.getMessage());
                competitorsEvidencePreferred = new SerperSearchResult(competitorsQuery2, "ERROR",
                        errorType + ": " + e.getMessage(), List.of());
            }

            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append(
                    "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n");
            promptBuilder.append(
                    "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ конкурентов и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
            promptBuilder.append(
                    "КРИТИЧЕСКИ ВАЖНО: Ниже даны результаты веб-поиска (Serper.dev). СНАЧАЛА проанализируй их, "
                            + "ЗАТЕМ сформируй JSON. Если WEB_SEARCH_STATUS=OK, используй ТОЛЬКО реальные названия конкурентов "
                            + "из WEB_SEARCH_RESULTS (никаких выдуманных брендов). Для каждого конкурента привяжи хотя бы 1 ссылку-источник.\n"
                            + "Если веб-поиск недоступен, можешь дать модели конкурентов, но ОБЯЗАТЕЛЬНО пометь это как \"Экстраполяция\" "
                            + "и не выдавай их за реальные компании.\n\n");
            promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n");
            promptBuilder.append("Ответ должен быть валидным JSON.\n\n");
            promptBuilder.append("WEB_SEARCH_RESULTS (для реальных конкурентов и источников):\n");
            promptBuilder.append("WEB_SEARCH_STATUS: competitors=")
                    .append(competitorsEvidence != null ? competitorsEvidence.status() : "ERROR")
                    .append(", preferred=")
                    .append(competitorsEvidencePreferred != null ? competitorsEvidencePreferred.status() : "ERROR")
                    .append("\n\n");
            appendSerperEvidenceToPrompt(promptBuilder, "COMPETITORS_EVIDENCE", competitorsEvidence, 10);
            appendSerperEvidenceToPrompt(promptBuilder, "COMPETITORS_EVIDENCE_PREFERRED", competitorsEvidencePreferred,
                    6);
            promptBuilder.append("---\n\n");
            promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n");
            promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n");
            promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (")
                    .append(request.getBusinessNiche()).append(")\n");
            promptBuilder.append("- Конкуренты (если указаны): ").append(request.getBusinessNiche())
                    .append(" (уточни основных конкурентов)\n");

            List<String> regions = request.getRegion();
            String regionStr = (regions != null && !regions.isEmpty())
                    ? String.join(", ", regions)
                    : "Не указан";
            promptBuilder.append("- Регион/город: ").append(regionStr).append("\n");
            promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n");

            promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n");
            promptBuilder.append("{\n");
            promptBuilder.append("  \"competitorAnalysis\": {\n");
            promptBuilder.append("    \"marketShareChart\": [ // Оценочные доли рынка для графика\n");
            promptBuilder.append("      { \"name\": \"Наш бренд\", \"value\": число },\n");
            promptBuilder.append("      { \"name\": \"Конкурент А\", \"value\": число },\n");
            promptBuilder.append("      { \"name\": \"Конкурент B\", \"value\": число },\n");
            promptBuilder.append("      { \"name\": \"Другие\", \"value\": число }\n");
            promptBuilder.append("    ], // сумма всех value должна быть = 100\n");
            promptBuilder.append(
                    "    \"marketShareComment\": \"Детальный анализ позиций на рынке (2-3 предложения с конкретными инсайтами)\",\n");
            promptBuilder.append("    \"competitorsDetails\": [ // Детали по 3-4 конкурентам\n");
            promptBuilder.append("      {\n");
            promptBuilder.append("        \"name\": \"Конкурент А\",\n");
            promptBuilder.append(
                    "        \"digitalMetrics\": { \"reach\": число, \"followers\": число, \"activityIndex\": число }, // activityIndex от 0 до 100\n");
            promptBuilder.append("        \"channelsHeatmap\": { // Оценка (0-100) качества ведения канала\n");
            promptBuilder.append(
                    "          \"Instagram\": число, \"TikTok\": число, \"Telegram\": число, \"YouTube\": число, \"Facebook\": число\n");
            promptBuilder.append("        },\n");
            promptBuilder.append("        \"priceStrategy\": \"Премиум/Средняя/Эконом\",\n");
            promptBuilder.append(
                    "        \"contentStrategy\": \"Детальное описание контент-стратегии конкурента (3-4 предложения). "
                            + "Опиши типы контента, стиль подачи, частоту публикаций, основные темы, тон коммуникации. "
                            + "Проанализируй, что работает у конкурента и почему.\",\n");
            promptBuilder.append("        \"swot\": {\n");
            promptBuilder.append(
                    "          \"strengths\": [\"Конкретная сильная сторона 1\", \"Конкретная сильная сторона 2\", \"Конкретная сильная сторона 3\"],\n");
            promptBuilder.append(
                    "          \"weaknesses\": [\"Конкретная слабая сторона 1\", \"Конкретная слабая сторона 2\", \"Конкретная слабая сторона 3\"]\n");
            promptBuilder.append("        }\n");
            promptBuilder.append("      }\n");
            promptBuilder.append("    ],\n");
            promptBuilder.append("    \"comparisonTable\": [ // Сравнительная таблица (5-6 характеристик)\n");
            promptBuilder.append(
                    "      { \"feature\": \"Цена\", \"us\": \"Средняя\", \"compA\": \"Высокая\", \"compB\": \"Низкая\" },\n");
            promptBuilder.append(
                    "      { \"feature\": \"Продукт\", \"us\": \"Онлайн\", \"compA\": \"Офлайн\", \"compB\": \"Гибрид\" },\n");
            promptBuilder.append(
                    "      { \"feature\": \"УТП\", \"us\": \"Сервис\", \"compA\": \"Бренд\", \"compB\": \"Дешевизна\" },\n");
            promptBuilder.append(
                    "      { \"feature\": \"Целевая аудитория\", \"us\": \"...\", \"compA\": \"...\", \"compB\": \"...\" },\n");
            promptBuilder.append(
                    "      { \"feature\": \"Каналы продвижения\", \"us\": \"...\", \"compA\": \"...\", \"compB\": \"...\" },\n");
            promptBuilder.append(
                    "      { \"feature\": \"Позиционирование\", \"us\": \"...\", \"compA\": \"...\", \"compB\": \"...\" }\n");
            promptBuilder.append("    ],\n");
            promptBuilder.append(
                    "    \"keyInsights\": [\"Глубокий инсайт 1 (1-2 предложения)\", \"Глубокий инсайт 2 (1-2 предложения)\"],\n");
            promptBuilder.append(
                    "    \"sources\": [ { \"title\": \"Источник (кратко)\", \"url\": \"https://...\", \"year\": \"2024/2025\" } ]\n");
            promptBuilder.append("  }\n");
            promptBuilder.append("}\n\n");
            promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n");
            promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n");
            promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n");
            promptBuilder.append("3. Сумма всех value в marketShareChart должна быть = 100.\n");
            promptBuilder.append("4. В массиве competitorsDetails должно быть 3-4 детальных описания конкурентов.\n");
            promptBuilder.append(
                    "5. Для каждого конкурента заполни ВСЕ поля: name, digitalMetrics, channelsHeatmap, priceStrategy, contentStrategy, swot.\n");
            promptBuilder.append(
                    "6. Поле contentStrategy должно содержать ДЕТАЛЬНОЕ описание (3-4 предложения), а не общие фразы.\n");
            promptBuilder.append(
                    "7. В swot для каждого конкурента укажи конкретные strengths и weaknesses (по 3-4 пункта), а не общие формулировки.\n");
            promptBuilder.append("8. В comparisonTable должно быть 5-6 строк сравнения различных характеристик.\n");
            promptBuilder.append(
                    "9. marketShareComment должен содержать детальный анализ позиций на рынке (2-3 предложения).\n");
            promptBuilder.append("10. keyInsights должны содержать глубокие инсайты, а не общие фразы.\n");
            promptBuilder.append("11. Все строки — на русском языке.\n");
            promptBuilder.append(
                    "12. channelsHeatmap должна содержать оценки (0-100) для всех основных каналов: Instagram, TikTok, Telegram, YouTube, Facebook.\n");
            promptBuilder.append(
                    "13. sources должен содержать реальные ссылки из WEB_SEARCH_RESULTS (если WEB_SEARCH_STATUS=OK). Если веб-поиск недоступен — sources можно оставить пустым, а выводы пометь как \"Экстраполяция\".\n");

            String prompt = promptBuilder.toString();
            logger.debug(
                    "getCompetitorAnalysis: Generating competitor analysis with detailed prompt (length: {} chars)",
                    prompt.length());
            logger.info(
                    "getCompetitorAnalysis: Calling OpenAI API for competitor analysis using miniModelName (gpt-4o-mini)");
            String jsonResponse = openAIAnalyticsService.generateWithInstructionWithModel(context, prompt, "ru",
                    miniModelName);
            logger.debug("getCompetitorAnalysis: OpenAI response received. Length: {} chars",
                    jsonResponse != null ? jsonResponse.length() : 0);

            if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
                logger.warn("getCompetitorAnalysis: Empty JSON response from AI for competitor analysis");
                return new HashMap<>();
            }

            logger.debug("getCompetitorAnalysis: Cleaning JSON from markdown");
            String cleanedJson = cleanJsonFromMarkdown(jsonResponse);
            logger.debug("getCompetitorAnalysis: Parsing JSON response");
            Map<String, Object> jsonMap = objectMapper.readValue(cleanedJson, new TypeReference<Map<String, Object>>() {
            });
            logger.debug("getCompetitorAnalysis: JSON parsed successfully. Keys: {}", jsonMap.keySet());

            logger.info("getCompetitorAnalysis: Competitor analysis generated successfully");
            return jsonMap;
        } catch (Exception e) {
            logger.error("getCompetitorAnalysis: Error generating competitor analysis: {}", e.getMessage(), e);
            logger.error("getCompetitorAnalysis: Stack trace: ", e);
            return new HashMap<>();
        }
    }

    /**
     * Генерация анализа сезонности и рынка
     */
    private Map<String, Object> getSeasonalityAndMarket(MarketingAnalysisRequest request) {
        String context = buildContextString(request);

        // Mandatory web search evidence (Serper.dev) for 2024-2025 market data
        String niche = request != null ? request.getBusinessNiche() : null;
        String product = request != null ? request.getProduct() : null;
        List<String> regionsForSearch = request != null ? request.getRegion() : null;
        String regionStrForSearch = (regionsForSearch != null && !regionsForSearch.isEmpty())
                ? String.join(", ", regionsForSearch)
                : "Казахстан";
        String topic = (niche != null && !niche.isBlank()) ? niche : (product != null ? product : "ниша");
        String preferredSites = "(site:stat.gov.kz OR site:kapital.kz OR site:kursiv.media OR site:forbes.kz OR site:pwc.kz OR site:kpmg.kz)";
        String marketQuery = topic + " рынок " + regionStrForSearch + " 2024 2025 статистика объем рост "
                + preferredSites;
        SerperSearchResult marketEvidence;
        try {
            marketEvidence = serperSearchService.search(marketQuery);
        } catch (Exception e) {
            // Handle DNS/network errors and other exceptions from Serper API
            String errorType = "Unexpected error";
            if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) {
                errorType = "DNS/Network failure";
            } else if (e instanceof java.util.concurrent.TimeoutException) {
                errorType = "Timeout";
            } else if (e instanceof java.io.IOException) {
                errorType = "IO error";
            }
            logger.warn("Serper API {} for market query '{}': {}", errorType, marketQuery, e.getMessage());
            marketEvidence = new SerperSearchResult(marketQuery, "ERROR", errorType + ": " + e.getMessage(), List.of());
        }

        String seasonalityQuery = topic + " сезонность спроса " + regionStrForSearch + " 2024 2025 " + preferredSites;
        SerperSearchResult seasonalityEvidence;
        try {
            seasonalityEvidence = serperSearchService.search(seasonalityQuery);
        } catch (Exception e) {
            // Handle DNS/network errors and other exceptions from Serper API
            String errorType = "Unexpected error";
            if (e instanceof java.net.UnknownHostException || e instanceof java.net.ConnectException) {
                errorType = "DNS/Network failure";
            } else if (e instanceof java.util.concurrent.TimeoutException) {
                errorType = "Timeout";
            } else if (e instanceof java.io.IOException) {
                errorType = "IO error";
            }
            logger.warn("Serper API {} for seasonality query '{}': {}", errorType, seasonalityQuery, e.getMessage());
            seasonalityEvidence = new SerperSearchResult(seasonalityQuery, "ERROR", errorType + ": " + e.getMessage(),
                    List.of());
        }

        StringBuilder promptBuilder = new StringBuilder();
        promptBuilder.append(
                "Ты — профессиональный маркетинговый аналитик, работающий с малым и средним бизнесом в Казахстане.\n\n");
        promptBuilder.append(
                "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ сезонности и рынка и выдать результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
        promptBuilder.append(
                "КРИТИЧЕСКИ ВАЖНО: Предоставь детальный анализ сезонных колебаний спроса с конкретными объяснениями. "
                        + "Опиши рыночные тренды, возможности и угрозы с конкретными инсайтами, а не общими фразами.\n\n");
        promptBuilder.append(
                "ОБЯЗАТЕЛЬНО (WEB SEARCH): ниже тебе даны результаты веб-поиска (Serper.dev) по 2024–2025 годам. "
                        + "СНАЧАЛА проанализируй эти данные, а ЗАТЕМ сформируй JSON. "
                        + "Если данные содержат числа — используй их. Если точных чисел нет — делай экспертную оценку через смежные рынки/ВВП/покупательную способность и помечай это как \"Экстраполяция\".\n");
        promptBuilder.append(
                "ЗАПРЕТ: нельзя использовать фиксированные заглушки или одинаковые оценки для разных ниш. Обувь не может стоить столько же, сколько видеокарты.\n\n");
        promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n");
        promptBuilder.append("Ответ должен быть валидным JSON.\n\n");
        promptBuilder.append("WEB_SEARCH_RESULTS (используй как доказательства и источники):\n");
        promptBuilder.append("WEB_SEARCH_STATUS: market=")
                .append(marketEvidence != null ? marketEvidence.status() : "ERROR")
                .append(", seasonality=")
                .append(seasonalityEvidence != null ? seasonalityEvidence.status() : "ERROR")
                .append("\n\n");
        appendSerperEvidenceToPrompt(promptBuilder, "MARKET_EVIDENCE", marketEvidence, 8);
        appendSerperEvidenceToPrompt(promptBuilder, "SEASONALITY_EVIDENCE", seasonalityEvidence, 6);
        promptBuilder.append("---\n\n");
        promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n");
        promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n");
        promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (")
                .append(request.getBusinessNiche()).append(")\n");

        List<String> regions = request.getRegion();
        String regionStr = (regions != null && !regions.isEmpty())
                ? String.join(", ", regions)
                : "Не указан";
        promptBuilder.append("- Регион/город: ").append(regionStr).append("\n");
        promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n");

        promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n");
        promptBuilder.append("{\n");
        promptBuilder.append("  \"seasonality\": {\n");
        promptBuilder.append(
                "    \"months\": [ \"Январь\", \"Февраль\", \"Март\", \"Апрель\", \"Май\", \"Июнь\", \"Июль\", \"Август\", \"Сентябрь\", \"Октябрь\", \"Ноябрь\", \"Декабрь\" ],\n");
        promptBuilder.append("    \"demandIndex\": [12 чисел от 0 до 100], // Индекс спроса по месяцам\n");
        promptBuilder.append(
                "    \"description\": \"Детальное описание сезонности (3-4 предложения). Опиши конкретные причины сезонных колебаний: "
                        + "праздники, климатические условия, культурные особенности, жизненные циклы целевой аудитории. "
                        + "Укажи, какие месяцы являются пиковыми и почему, какие - низкими и что это означает для бизнеса.\"\n");
        promptBuilder.append("  },\n");
        promptBuilder.append("  \"market\": {\n");
        promptBuilder.append(
                "    \"marketSize\": \"Оценка объема рынка (например, 'Оценка объема рынка составляет X млн тенге в год' или 'Средний рынок с потенциалом роста'). "
                        + "Дай конкретную оценку или описание масштаба рынка в Казахстане для данной ниши.\",\n");
        promptBuilder.append(
                "    \"growthRate\": \"Темп роста рынка (например, 'Умеренный рост X% в год' или 'Стабильный рост на уровне инфляции'). Укажи оценку/диапазон и кратко обоснуй.\",\n");
        promptBuilder.append(
                "    \"sources\": [ { \"title\": \"Источник (кратко)\", \"url\": \"https://...\", \"year\": \"2024/2025\" } ],\n");
        promptBuilder.append(
                "    \"extrapolationMethod\": \"Если точных данных нет, опиши метод экспертной оценки и пометь как Экстраполяция.\",\n");
        promptBuilder.append(
                "    \"trends\": [\"Конкретный тренд 1 (1-2 предложения с деталями)\", \"Конкретный тренд 2 (1-2 предложения с деталями)\", \"Конкретный тренд 3 (1-2 предложения с деталями)\"],\n");
        promptBuilder.append(
                "    \"opportunities\": [\"Конкретная возможность 1 (1-2 предложения с деталями)\", \"Конкретная возможность 2 (1-2 предложения с деталями)\"],\n");
        promptBuilder.append(
                "    \"threats\": [\"Конкретная угроза 1 (1-2 предложения с деталями)\", \"Конкретная угроза 2 (1-2 предложения с деталями)\"]\n");
        promptBuilder.append("  }\n");
        promptBuilder.append("}\n\n");
        promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n");
        promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n");
        promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n");
        promptBuilder
                .append("3. Значения \"demandIndex\" должны быть от 0 до 100 (12 чисел для каждого месяца).\n");
        promptBuilder.append(
                "4. Поле \"description\" в seasonality должно содержать ДЕТАЛЬНОЕ описание (3-4 предложения) с конкретными причинами сезонных колебаний.\n");
        promptBuilder.append(
                "5. Поле \"marketSize\" должно содержать конкретную оценку объема рынка, а не просто слова 'средний' или 'крупный'.\n");
        promptBuilder.append(
                "6. Массивы trends, opportunities, threats должны содержать КОНКРЕТНЫЕ инсайты (по 1-2 предложения каждый), а не общие фразы.\n");
        promptBuilder.append(
                "7. Каждый элемент в trends должен описывать реальный тренд с конкретными деталями (например, 'Рост популярности онлайн-заказов на 30% за последний год').\n");
        promptBuilder.append(
                "8. Каждый элемент в opportunities должен описывать конкретную возможность с практическими деталями.\n");
        promptBuilder.append(
                "9. Каждый элемент в threats должен описывать конкретную угрозу с практическими деталями.\n");
        promptBuilder.append("10. Все строки — на русском языке.\n");
        promptBuilder.append(
                "11. НЕ включай поле \"estimated\" ни в seasonality, ни в market.\n");
        promptBuilder.append(
                "12. sources должен содержать реальные ссылки из WEB_SEARCH_RESULTS (если WEB_SEARCH_STATUS=OK). Если веб-поиск недоступен — укажи extrapolationMethod как \"Экстраполяция\" и оставь sources пустым массивом.\n");

        String prompt = promptBuilder.toString();
        logger.debug(
                "getSeasonalityAndMarket: Generating seasonality and market analysis with detailed prompt (length: {} chars)",
                prompt.length());

        // Retry logic for JsonParseException
        int attempt = 0;
        while (attempt < MAX_RETRY_ATTEMPTS) {
            try {
                attempt++;
                logger.info(
                        "getSeasonalityAndMarket: Calling OpenAI API for seasonality and market analysis (attempt {}/{}) using miniModelName (gpt-4o-mini)",
                        attempt, MAX_RETRY_ATTEMPTS);
                String jsonResponse = openAIAnalyticsService.generateWithInstructionWithModel(context, prompt, "ru",
                        miniModelName);
                logger.debug("getSeasonalityAndMarket: OpenAI response received. Length: {} chars",
                        jsonResponse != null ? jsonResponse.length() : 0);

                if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
                    logger.warn(
                            "getSeasonalityAndMarket: Empty JSON response from AI for seasonality and market analysis");
                    return new HashMap<>();
                }

                logger.debug("getSeasonalityAndMarket: Cleaning JSON from markdown");
                String cleanedJson = cleanJsonFromMarkdown(jsonResponse);
                logger.debug("getSeasonalityAndMarket: Parsing JSON response");
                Map<String, Object> jsonMap = objectMapper.readValue(cleanedJson,
                        new TypeReference<Map<String, Object>>() {
                        });
                logger.debug("getSeasonalityAndMarket: JSON parsed successfully. Keys: {}", jsonMap.keySet());

                logger.info("getSeasonalityAndMarket: Seasonality and market analysis generated successfully");
                return jsonMap;
            } catch (JsonParseException e) {
                logger.warn("getSeasonalityAndMarket: JsonParseException on attempt {}/{}: {}. Will retry...",
                        attempt, MAX_RETRY_ATTEMPTS, e.getMessage());
                if (attempt < MAX_RETRY_ATTEMPTS) {
                    try {
                        Thread.sleep(RETRY_DELAY_MS);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        logger.error("getSeasonalityAndMarket: Retry delay interrupted", ie);
                        return new HashMap<>();
                    }
                } else {
                    logger.error("getSeasonalityAndMarket: JsonParseException after {} attempts: {}",
                            MAX_RETRY_ATTEMPTS, e.getMessage(), e);
                    logger.error("getSeasonalityAndMarket: Stack trace: ", e);
                    return new HashMap<>();
                }
            } catch (Exception e) {
                logger.error("getSeasonalityAndMarket: Error generating seasonality and market analysis: {}",
                        e.getMessage(), e);
                logger.error("getSeasonalityAndMarket: Stack trace: ", e);
                return new HashMap<>();
            }
        }

        logger.error("getSeasonalityAndMarket: Failed after {} attempts", MAX_RETRY_ATTEMPTS);
        return new HashMap<>();
    }

    private Map<String, Object> getStrategyAndFunnel(MarketingAnalysisRequest request) {
        try {
            String context = buildContextString(request);

            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append(
                    "Ты — профессиональный маркетинговый аналитик и стратег, работающий с малым и средним бизнесом в Казахстане.\n\n");
            promptBuilder.append(
                    "Твоя задача — на основе входных данных о бизнесе выполнить глубокий анализ стратегии, воронки конверсии, потенциала каналов продвижения и SWOT-анализа бизнеса.\n\n");
            promptBuilder.append(
                    "КРИТИЧЕСКИ ВАЖНО: Предоставь детальный анализ с конкретными цифрами и инсайтами. "
                            + "Воронка должна отражать реалистичные этапы конверсии для данного бизнеса. "
                            + "Каналы должны быть оценены по их реальному потенциалу для данной ниши в Казахстане. "
                            + "SWOT должен содержать конкретные сильные и слабые стороны, возможности и угрозы.\n\n");
            promptBuilder.append(
                    "Выдай результат ТОЛЬКО в виде корректного JSON-объекта строго по указанной структуре.\n\n");
            promptBuilder.append("Не добавляй пояснений, комментариев, текста до или после JSON.\n");
            promptBuilder.append("Ответ должен быть валидным JSON.\n\n");
            promptBuilder.append("---\n\n");
            promptBuilder.append("ВХОДНЫЕ ДАННЫЕ ПОЛЬЗОВАТЕЛЯ:\n\n");
            promptBuilder.append("- Название компании/продукта: ").append(request.getProduct()).append("\n");
            promptBuilder.append("- Описание продукта/услуг: ").append(request.getProduct()).append(" (")
                    .append(request.getBusinessNiche()).append(")\n");

            List<String> regions = request.getRegion();
            String regionStr = (regions != null && !regions.isEmpty())
                    ? String.join(", ", regions)
                    : "Не указан";
            promptBuilder.append("- Регион/город: ").append(regionStr).append("\n");
            promptBuilder.append("- Цель продвижения: ").append(request.getGoal()).append("\n\n");

            promptBuilder.append("СФОРМИРУЙ JSON СЛЕДУЮЩЕЙ СТРУКТУРЫ:\n\n");
            promptBuilder.append("{\n");
            promptBuilder.append("  \"strategyAnalysis\": {\n");
            promptBuilder.append("    \"funnel\": {\n");
            promptBuilder.append("      \"stages\": [\n");
            promptBuilder.append("        {\n");
            promptBuilder.append("          \"stageName\": \"Охват\",\n");
            promptBuilder.append("          \"value\": число, // Количество людей на этапе\n");
            promptBuilder.append(
                    "          \"conversionPercent\": 100 // Процент конверсии (для первого этапа всегда 100)\n");
            promptBuilder.append("        },\n");
            promptBuilder.append("        {\n");
            promptBuilder.append("          \"stageName\": \"Интерес\",\n");
            promptBuilder.append("          \"value\": число, // Количество людей на этапе\n");
            promptBuilder.append("          \"conversionPercent\": число // Процент конверсии от предыдущего этапа\n");
            promptBuilder.append("        },\n");
            promptBuilder.append("        {\n");
            promptBuilder.append("          \"stageName\": \"Заявка\",\n");
            promptBuilder.append("          \"value\": число,\n");
            promptBuilder.append("          \"conversionPercent\": число\n");
            promptBuilder.append("        },\n");
            promptBuilder.append("        {\n");
            promptBuilder.append("          \"stageName\": \"Покупка\",\n");
            promptBuilder.append("          \"value\": число,\n");
            promptBuilder.append("          \"conversionPercent\": число\n");
            promptBuilder.append("        }\n");
            promptBuilder.append("      ],\n");
            promptBuilder.append(
                    "      \"analysis\": \"Детальный анализ воронки конверсии (3-4 предложения). Опиши, где находится узкое место воронки, какие этапы имеют низкую конверсию и почему. Дай конкретные рекомендации по улучшению каждого этапа.\"\n");
            promptBuilder.append("    },\n");
            promptBuilder.append("    \"channelsPotential\": [\n");
            promptBuilder.append("      {\n");
            promptBuilder.append("        \"name\": \"Instagram\",\n");
            promptBuilder.append("        \"potentialIndex\": число, // Индекс потенциала от 0 до 100\n");
            promptBuilder.append("        \"priority\": \"High\" // High, Medium или Low\n");
            promptBuilder.append("      },\n");
            promptBuilder.append("      {\n");
            promptBuilder.append("        \"name\": \"Google Ads\",\n");
            promptBuilder.append("        \"potentialIndex\": число,\n");
            promptBuilder.append("        \"priority\": \"Medium\"\n");
            promptBuilder.append("      },\n");
            promptBuilder.append("      {\n");
            promptBuilder.append("        \"name\": \"TikTok\",\n");
            promptBuilder.append("        \"potentialIndex\": число,\n");
            promptBuilder.append("        \"priority\": \"Medium\"\n");
            promptBuilder.append("      }\n");
            promptBuilder.append("    ],\n");
            promptBuilder.append("    \"globalSwot\": {\n");
            promptBuilder.append(
                    "      \"strengths\": [\"Конкретная сильная сторона 1 (1-2 предложения)\", \"Конкретная сильная сторона 2 (1-2 предложения)\", \"Конкретная сильная сторона 3 (1-2 предложения)\"],\n");
            promptBuilder.append(
                    "      \"weaknesses\": [\"Конкретная слабая сторона 1 (1-2 предложения)\", \"Конкретная слабая сторона 2 (1-2 предложения)\", \"Конкретная слабая сторона 3 (1-2 предложения)\"],\n");
            promptBuilder.append(
                    "      \"opportunities\": [\"Конкретная возможность 1 (1-2 предложения)\", \"Конкретная возможность 2 (1-2 предложения)\", \"Конкретная возможность 3 (1-2 предложения)\"],\n");
            promptBuilder.append(
                    "      \"threats\": [\"Конкретная угроза 1 (1-2 предложения)\", \"Конкретная угроза 2 (1-2 предложения)\", \"Конкретная угроза 3 (1-2 предложения)\"]\n");
            promptBuilder.append("    }\n");
            promptBuilder.append("  }\n");
            promptBuilder.append("}\n\n");
            promptBuilder.append("КРИТИЧЕСКИ ВАЖНЫЕ ТРЕБОВАНИЯ:\n\n");
            promptBuilder.append("1. Верни ТОЛЬКО JSON, без пояснений, комментариев, markdown-разметки.\n");
            promptBuilder.append("2. JSON должен быть синтаксически корректным и валидным.\n");
            promptBuilder.append("3. Воронка должна содержать минимум 4 этапа с реалистичными значениями.\n");
            promptBuilder.append(
                    "4. Значения в funnel.stages должны быть логичными: каждый следующий этап должен иметь меньшее или равное значение value по сравнению с предыдущим.\n");
            promptBuilder.append("5. conversionPercent должен отражать процент конверсии от предыдущего этапа.\n");
            promptBuilder.append(
                    "6. channelsPotential должен содержать минимум 3-5 каналов с оценкой их потенциала для данного бизнеса.\n");
            promptBuilder.append("7. potentialIndex должен быть числом от 0 до 100.\n");
            promptBuilder.append("8. priority должен быть одним из: \"High\", \"Medium\", \"Low\".\n");
            promptBuilder.append(
                    "9. globalSwot должен содержать конкретные пункты (по 3-4 в каждой категории), а не общие фразы.\n");
            promptBuilder.append(
                    "10. Поле analysis в funnel должно содержать детальный анализ (3-4 предложения) с конкретными рекомендациями.\n");
            promptBuilder.append("11. Все строки — на русском языке.\n");

            String prompt = promptBuilder.toString();
            logger.debug(
                    "getStrategyAndFunnel: Generating strategy and funnel analysis with detailed prompt (length: {} chars)",
                    prompt.length());

            // Retry logic for JsonParseException
            int attempt = 0;
            while (attempt < MAX_RETRY_ATTEMPTS) {
                try {
                    attempt++;
                    logger.info(
                            "getStrategyAndFunnel: Calling OpenAI API for strategy and funnel analysis (attempt {}/{}) using miniModelName (gpt-4o-mini)",
                            attempt, MAX_RETRY_ATTEMPTS);
                    String jsonResponse = openAIAnalyticsService.generateWithInstructionWithModel(context, prompt, "ru",
                            miniModelName);
                    logger.debug("getStrategyAndFunnel: OpenAI response received. Length: {} chars",
                            jsonResponse != null ? jsonResponse.length() : 0);

                    if (jsonResponse == null || jsonResponse.trim().isEmpty()) {
                        logger.warn(
                                "getStrategyAndFunnel: Empty JSON response from AI for strategy and funnel analysis");
                        return new HashMap<>();
                    }

                    logger.debug("getStrategyAndFunnel: Cleaning JSON from markdown");
                    String cleanedJson = cleanJsonFromMarkdown(jsonResponse);
                    logger.debug("getStrategyAndFunnel: Parsing JSON response");
                    Map<String, Object> jsonMap = objectMapper.readValue(cleanedJson,
                            new TypeReference<Map<String, Object>>() {
                            });
                    logger.debug("getStrategyAndFunnel: JSON parsed successfully. Keys: {}", jsonMap.keySet());

                    logger.info("getStrategyAndFunnel: Strategy and funnel analysis generated successfully");
                    return jsonMap;
                } catch (JsonParseException e) {
                    logger.warn("getStrategyAndFunnel: JsonParseException on attempt {}/{}: {}. Will retry...",
                            attempt, MAX_RETRY_ATTEMPTS, e.getMessage());
                    if (attempt < MAX_RETRY_ATTEMPTS) {
                        try {
                            Thread.sleep(RETRY_DELAY_MS);
                        } catch (InterruptedException ie) {
                            Thread.currentThread().interrupt();
                            logger.error("getStrategyAndFunnel: Retry delay interrupted", ie);
                            return new HashMap<>();
                        }
                    } else {
                        logger.error("getStrategyAndFunnel: JsonParseException after {} attempts: {}",
                                MAX_RETRY_ATTEMPTS, e.getMessage(), e);
                        logger.error("getStrategyAndFunnel: Stack trace: ", e);
                        return new HashMap<>();
                    }
                } catch (Exception e) {
                    logger.error("getStrategyAndFunnel: Error generating strategy and funnel analysis: {}",
                            e.getMessage(), e);
                    logger.error("getStrategyAndFunnel: Stack trace: ", e);
                    return new HashMap<>();
                }
            }

            logger.error("getStrategyAndFunnel: Failed after {} attempts", MAX_RETRY_ATTEMPTS);
            return new HashMap<>();
        } catch (Exception e) {
            logger.error("getStrategyAndFunnel: Error generating strategy and funnel analysis: {}", e.getMessage(), e);
            logger.error("getStrategyAndFunnel: Stack trace: ", e);
            return new HashMap<>();
        }
    }

    // Удалены старые/неиспользуемые генераторы отчёта:
    // - generateTextReportFromJson(...)
    // - generateDeepTextReport(...)
    // Актуальная генерация отчёта выполняется через модульные genSection* методы +
    // сборку в generateMarketingReport().

    /**
     * Проверяет, является ли ответ от AI валидным (не является отказом или ошибкой)
     * 
     * @param result Ответ от AI
     * @return true если ответ валиден, false если это отказ или ошибка
     */
    private boolean isValidAiResponse(String result) {
        if (result == null || result.trim().isEmpty()) {
            return false;
        }

        String lowerResult = result.toLowerCase().trim();

        // Явные "заглушки" о провале генерации (не считаем валидным ответом модели)
        // Важно: это нужно, чтобы такие ответы не считались "успешной генерацией" при
        // сборке отчёта.
        if (lowerResult.contains("не удалось сгенерировать раздел") ||
                lowerResult.contains("ошибка при генерации раздела") ||
                lowerResult.startsWith("ошибка при генерации") ||
                lowerResult.startsWith("error generating") ||
                lowerResult.startsWith("error:")) {
            return false;
        }

        // Проверяем на типичные ответы отказа от OpenAI
        if (lowerResult.contains("i'm sorry") ||
                lowerResult.contains("i cannot") ||
                lowerResult.contains("i can't assist") ||
                lowerResult.contains("i cannot assist") ||
                lowerResult.contains("i'm unable to") ||
                lowerResult.contains("i cannot help") ||
                lowerResult.contains("i can't help") ||
                lowerResult.startsWith("sorry") ||
                (lowerResult.contains("cannot") && lowerResult.contains("assist")) ||
                (lowerResult.contains("can't") && lowerResult.contains("assist"))) {
            logger.warn("isValidAiResponse: AI returned refusal message: {}",
                    result.substring(0, Math.min(100, result.length())));
            return false;
        }

        // Проверяем, что ответ содержит хотя бы минимальное содержание
        if (result.trim().length() < 50) {
            logger.warn("isValidAiResponse: Response too short: {} chars", result.length());
            return false;
        }

        return true;
    }

    private String generateWithRetryForSection(
            Map<String, Object> contextJson,
            String jsonContext,
            String prompt,
            String lang,
            String primaryModel,
            String fallbackModel,
            int attempts,
            String sectionId) {
        return generateWithRetryForSection(contextJson, jsonContext, prompt, lang, primaryModel, fallbackModel,
                attempts, sectionId, null);
    }

    private String generateWithRetryForSection(
            Map<String, Object> contextJson,
            String jsonContext,
            String prompt,
            String lang,
            String primaryModel,
            String fallbackModel,
            int attempts,
            String sectionId,
            Long timeoutOverrideMs) {
        try {

            int maxAttempts = Math.max(1, attempts);
            String last = null;
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            String systemPrompt = buildSystemPromptForSection(detailLevel, sectionId);
            Integer maxTokensOverride = detailLevel == DetailLevel.ПОДРОБНО ? REPORT_DETAILED_MAX_TOKENS : null;

            for (int i = 1; i <= maxAttempts; i++) {
                String modelToUse = primaryModel;
                // На последней попытке можно переключиться на запасную модель (если задана)
                if (i == maxAttempts && fallbackModel != null && !fallbackModel.isBlank()) {
                    modelToUse = fallbackModel;
                }

                boolean is429Error = false;
                try {
                    // Acquire semaphore permit before making OpenAI API call
                    if (requestSemaphore != null) {
                        try {
                            requestSemaphore.acquire();
                            logger.debug("Acquired semaphore permit for section '{}' (attempt {}/{})", sectionId, i,
                                    maxAttempts);
                        } catch (InterruptedException ie) {
                            Thread.currentThread().interrupt();
                            logger.warn("Interrupted while waiting for semaphore permit");
                            break;
                        }
                    }

                    try {
                        last = openAIAnalyticsService.generateWithInstructionWithModel(
                                jsonContext, prompt, lang, modelToUse, systemPrompt, maxTokensOverride,
                                timeoutOverrideMs);
                    } catch (Exception e) {
                        // Check if it's a 429 rate limit error
                        if (e instanceof WebClientResponseException) {
                            WebClientResponseException wcre = (WebClientResponseException) e;
                            if (wcre.getStatusCode().value() == 429) {
                                is429Error = true;
                                logger.warn(
                                        "generateWithRetryForSection: attempt {} failed with 429 rate limit for section '{}'",
                                        i, sectionId);
                            }
                        }
                        logger.warn("generateWithRetry: attempt {} failed with exception: {} for section '{}'", i,
                                e.getMessage(), sectionId);
                        last = null;
                    } finally {
                        // Always release semaphore permit
                        if (requestSemaphore != null) {
                            requestSemaphore.release();
                            logger.debug("Released semaphore permit for section '{}' (attempt {}/{})", sectionId, i,
                                    maxAttempts);
                        }
                    }
                } catch (Exception e) {
                    // Ensure semaphore is released even if there's an unexpected error
                    if (requestSemaphore != null) {
                        requestSemaphore.release();
                    }
                    logger.warn("generateWithRetry: attempt {} failed with exception: {} for section '{}'", i,
                            e.getMessage(), sectionId);
                    last = null;
                }

                if (isValidAiResponse(last)) {
                    return last;
                }

                // Exponential backoff for 429 errors or empty responses
                if (i < maxAttempts) {
                    long delayMs;
                    if (is429Error) {
                        // Увеличенные задержки для 429 ошибок: 10s, 20s (приоритет стабильности)
                        delayMs = i * 10000; // 10 секунд для первой попытки, 20 секунд для второй
                        logger.info("Waiting {}ms before retry (attempt {}/{}) for section '{}' due to 429 rate limit",
                                delayMs, i, maxAttempts, sectionId);
                    } else if (last == null && i < maxAttempts) {
                        // Для пустых ответов используем стандартную экспоненциальную задержку
                        delayMs = (long) Math.pow(2, i) * 1000; // 2s, 4s, 8s
                        logger.info("Waiting {}ms before retry (attempt {}/{}) for section '{}' due to empty response",
                                delayMs, i, maxAttempts, sectionId);
                    } else {
                        // Small delay for other errors
                        delayMs = 250L;
                    }
                    try {
                        Thread.sleep(delayMs);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        break;
                    }
                }
            }

            return last;
        } catch (Exception e) {
            logger.error("generateWithRetryForSection: Error generating section '{}': {}", sectionId, e.getMessage(),
                    e);
            logger.error("generateWithRetryForSection: Stack trace: ", e);
            return null;
        }
    }

    // Backward-compatible helper (legacy signature). Kept because file historically
    // used it;
    // new code should prefer generateWithRetryForSection(...).
    @SuppressWarnings("unused")
    private String generateWithRetry(String jsonContext, String prompt, String lang, String primaryModel,
            String fallbackModel, int attempts) {
        return generateWithRetryForSection(null, jsonContext, prompt, lang, primaryModel, fallbackModel, attempts,
                null);
    }

    /**
     * Удаляет сообщения об ошибках и отказах AI из текста отчета
     * 
     * @param text Текст отчета
     * @return Очищенный текст без сообщений об ошибках
     */
    private String removeErrorMessages(String text) {
        if (text == null || text.trim().isEmpty()) {
            return text;
        }

        String cleaned = text;

        // Паттерны для удаления (включая различные варианты)
        String[] errorPatterns = {
                "I'm sorry, but I can't assist with that request.",
                "I'm sorry, but I can't assist with that request",
                "I'm sorry, but I cannot assist with that request.",
                "I'm sorry, but I cannot assist with that request",
                "I'm sorry, but I can't help with that request.",
                "I'm sorry, but I can't help with that request",
                "I'm sorry, but I cannot help with that request.",
                "I'm sorry, but I cannot help with that request",
                "I'm sorry, but I'm unable to assist with that request.",
                "I'm sorry, but I'm unable to assist with that request",
                "Sorry, but I can't assist with that request.",
                "Sorry, but I can't assist with that request",
                "I cannot assist with that request.",
                "I cannot assist with that request",
                "I can't assist with that request.",
                "I can't assist with that request",
                // Русские заглушки/ошибки генерации
                "Не удалось сгенерировать раздел.",
                "Не удалось сгенерировать раздел",
                "Ошибка при генерации раздела.",
                "Ошибка при генерации раздела",
                "Раздел пропущен:",
                "Раздел пропущен"
        };

        for (String pattern : errorPatterns) {
            // Удаляем паттерн в любом регистре (case-insensitive)
            String quotedPattern = java.util.regex.Pattern.quote(pattern);
            // Удаляем паттерн как есть
            cleaned = cleaned.replaceAll("(?i)" + quotedPattern, "");
            // Также удаляем с пробелами и переносами строк вокруг
            cleaned = cleaned.replaceAll("(?i)(?m)^\\s*" + quotedPattern + "\\s*$", "");
            cleaned = cleaned.replaceAll("(?i)\\s+" + quotedPattern + "\\s+", " ");
            cleaned = cleaned.replaceAll("(?i)" + quotedPattern + "\\s*", "");
        }

        // Удаляем множественные пустые строки
        cleaned = cleaned.replaceAll("\\n{3,}", "\n\n");

        // Удаляем пробелы в начале и конце
        cleaned = cleaned.trim();

        if (!cleaned.equals(text)) {
            logger.warn(
                    "removeErrorMessages: Removed error messages from text. Original length: {}, Cleaned length: {}",
                    text.length(), cleaned.length());
        }

        return cleaned;
    }

    /**
     * Жесткая санитизация: убирает запрещённые фразы про отсутствие данных и, при
     * необходимости, хвостовые плейсхолдеры [[CHART_*]]/[[TABLE_*]].
     *
     * Цель: в финальном отчёте не должно быть ни одной фразы "данные
     * отсутствуют"/"нет данных"
     * и не должно оставаться токенов [[CHART_...]].
     */
    private String hardSanitizeReportText(String text, boolean removePlaceholders) {
        if (text == null || text.isBlank()) {
            return text;
        }

        String cleaned = text;

        // 1) Replace "no data" sentences with a neutral analytical extrapolation
        // statement.
        String[] noDataSentencePatterns = new String[] {
                "(?i)\\bк\\s*сожалению\\b[^\\n\\.\\!\\?]*\\bданн[а-яё]*\\b[^\\n\\.\\!\\?]*(нет|не\\s*представлен[а-яё]*)[^\\n\\.\\!\\?]*[\\.\\!\\?]?",
                "(?i)\\bданные\\s*(отсутствуют|не\\s*представлен[а-яё]*|не\\s*доступн[а-яё]*)\\b[^\\n\\.\\!\\?]*[\\.\\!\\?]?",
                "(?i)\\bнет\\s*данных\\b[^\\n\\.\\!\\?]*[\\.\\!\\?]?",
                "(?i)\\bнет\\s*информации\\b[^\\n\\.\\!\\?]*[\\.\\!\\?]?"
        };
        for (String p : noDataSentencePatterns) {
            cleaned = cleaned.replaceAll(p, "Экстраполяция: применяем консервативные допущения и методику расчета.");
        }
        cleaned = cleaned.replaceAll("(?i)\\b(нет\\s*данных|данные\\s*отсутствуют|нет\\s*информации)\\b", "");
        if (removePlaceholders) {
            cleaned = cleaned.replaceAll("(?i)\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_]+\\s*\\]\\]", "");
        }
        cleaned = cleaned.replaceAll("(\\r?\\n){3,}", "\n\n").trim();
        return cleaned;
    }

    private String removeDanglingChartNarration(String text) {
        if (text == null || text.trim().isEmpty()) {
            return text;
        }

        String[] needles = {
                "Анализ данных на графике ниже показывает",
                "Из этих показателей следует, что"
        };

        String[] lines = text.split("\\r?\\n", -1);
        List<String> out = new ArrayList<>(lines.length);

        java.util.regex.Pattern placeholderPattern = java.util.regex.Pattern
                .compile("\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_]+\\s*\\]\\]", java.util.regex.Pattern.CASE_INSENSITIVE);
        java.util.regex.Pattern jsonBlockStartPattern = java.util.regex.Pattern
                .compile("```\\s*json\\s*:", java.util.regex.Pattern.CASE_INSENSITIVE);

        for (int i = 0; i < lines.length; i++) {
            String line = lines[i];
            String trimmed = line != null ? line.trim() : "";

            boolean isNarration = false;
            for (String n : needles) {
                if (trimmed.toLowerCase().contains(n.toLowerCase())) {
                    isNarration = true;
                    break;
                }
            }

            if (!isNarration) {
                out.add(line);
                continue;
            }

            // Keep narration only if there is a placeholder or json block nearby (next 3
            // lines or same line).
            boolean hasVisualNear = false;
            int from = Math.max(0, i);
            int to = Math.min(lines.length - 1, i + 3);
            for (int j = from; j <= to; j++) {
                String ctx = lines[j] != null ? lines[j] : "";
                if (placeholderPattern.matcher(ctx).find() || jsonBlockStartPattern.matcher(ctx).find()) {
                    hasVisualNear = true;
                    break;
                }
            }

            if (hasVisualNear) {
                out.add(line);
            } else {
                // Drop this line (and if it's alone in a paragraph, later normalization will
                // collapse empty lines)
                continue;
            }
        }

        String cleaned = String.join("\n", out);
        cleaned = cleaned.replaceAll("\\n{3,}", "\n\n").trim();
        return cleaned;
    }

    /**
     * Подготавливает контекстный JSON для конкретного раздела отчета.
     * Извлекает только релевантные данные для экономии токенов.
     * 
     * @param fullJson Полный JSON анализа
     * @param section  Название раздела (summary, product, market, audience,
     *                 competitors, swot, channels, funnel, positioning, content,
     *                 strategy)
     * @return Контекстный JSON с базовыми полями и специфичными для раздела данными
     */
    private Map<String, Object> prepareSectionContext(Map<String, Object> fullJson, String section) {
        Map<String, Object> context = new HashMap<>();

        // Всегда включаем базовые поля
        if (fullJson.containsKey("product")) {
            context.put("product", fullJson.get("product"));
        }
        if (fullJson.containsKey("businessNiche")) {
            context.put("businessNiche", fullJson.get("businessNiche"));
        }
        if (fullJson.containsKey("goal")) {
            context.put("goal", fullJson.get("goal"));
        }
        if (fullJson.containsKey("regions")) {
            context.put("regions", fullJson.get("regions"));
        }
        if (fullJson.containsKey("detailLevel")) {
            context.put("detailLevel", fullJson.get("detailLevel"));
        }
        if (fullJson.containsKey("researchPack") && shouldIncludeResearchPackInSection(section)) {
            context.put("researchPack", fullJson.get("researchPack"));
        }

        // Добавляем специфичные данные для каждого раздела
        switch (section) {
            case "summary":
                // Для резюме достаточно базовых полей + meta если есть
                if (fullJson.containsKey("meta")) {
                    context.put("meta", fullJson.get("meta"));
                }
                // Резюме обязано опираться на рынок/сезонность (динамические значения +
                // источники)
                if (fullJson.containsKey("seasonality")) {
                    context.put("seasonality", fullJson.get("seasonality"));
                }
                if (fullJson.containsKey("market")) {
                    context.put("market", fullJson.get("market"));
                }
                break;
            case "product":
                // Для продукта добавляем позиционирование и УТП если есть
                if (fullJson.containsKey("positioning")) {
                    context.put("positioning", fullJson.get("positioning"));
                }
                if (fullJson.containsKey("valueProposition")) {
                    context.put("valueProposition", fullJson.get("valueProposition"));
                }
                // Раздел продукта также должен корректно ссылаться на рынок (без заглушек)
                if (fullJson.containsKey("market")) {
                    context.put("market", fullJson.get("market"));
                }
                break;
            case "market":
                // ОБЯЗАТЕЛЬНО: seasonality + market должны доходить до секции рынка
                if (fullJson.containsKey("seasonality")) {
                    context.put("seasonality", fullJson.get("seasonality"));
                }
                if (fullJson.containsKey("market")) {
                    context.put("market", fullJson.get("market"));
                }
                break;
            case "audience":
                // Новая структура: audienceAnalysis (ageGroups, genderDistribution, segments,
                // keyTakeaways, etc.)
                if (fullJson.containsKey("audienceAnalysis")) {
                    // ОБЯЗАТЕЛЬНО: audienceAnalysis должен доходить до секции аудитории
                    context.put("audienceAnalysis", fullJson.get("audienceAnalysis"));
                } else if (fullJson.containsKey("audienceSegments")) {
                    // Backward compatibility (старая структура)
                    context.put("audienceSegments", fullJson.get("audienceSegments"));
                }
                break;
            case "competitors":
                // Новая структура: competitorAnalysis (marketShareChart, comparisonTable,
                // competitorsDetails, swotCompetitors, etc.)
                if (fullJson.containsKey("competitorAnalysis")) {
                    Object competitorAnalysisObj = fullJson.get("competitorAnalysis");
                    // ОБЯЗАТЕЛЬНО: competitorAnalysis должен доходить до секции конкурентов
                    context.put("competitorAnalysis", competitorAnalysisObj);
                    // Aliases для совместимости: плоские ключи для секции конкурентов
                    // (данные физически лежат в competitorAnalysis.*)
                    if (competitorAnalysisObj instanceof Map) {
                        @SuppressWarnings("unchecked")
                        Map<String, Object> competitorAnalysis = (Map<String, Object>) competitorAnalysisObj;
                        if (competitorAnalysis != null) {
                            if (competitorAnalysis.containsKey("marketShareChart")) {
                                context.put("marketShareChart", competitorAnalysis.get("marketShareChart"));
                            }
                            if (competitorAnalysis.containsKey("comparisonTable")) {
                                context.put("comparisonTable", competitorAnalysis.get("comparisonTable"));
                            }
                        }
                    }
                } else {
                    // Backward compatibility (старая структура)
                    if (fullJson.containsKey("competitors")) {
                        context.put("competitors", fullJson.get("competitors"));
                    }
                    if (fullJson.containsKey("comparisonTable")) {
                        context.put("comparisonTable", fullJson.get("comparisonTable"));
                    }
                    if (fullJson.containsKey("swotCompetitors")) {
                        context.put("swotCompetitors", fullJson.get("swotCompetitors"));
                    }
                    if (fullJson.containsKey("marketShareChart")) {
                        context.put("marketShareChart", fullJson.get("marketShareChart"));
                    }
                }
                break;
            case "swot":
                // Новая структура: strategyAnalysis.globalSwot
                if (fullJson.containsKey("swot")) {
                    context.put("swot", fullJson.get("swot"));
                } else if (fullJson.containsKey("strategyAnalysis")) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> strategyAnalysis = (Map<String, Object>) fullJson.get("strategyAnalysis");
                    if (strategyAnalysis != null && strategyAnalysis.get("globalSwot") != null) {
                        context.put("swot", strategyAnalysis.get("globalSwot"));
                    }
                }
                // Даже если swot отсутствует/пустой — даём опорный контекст (для гипотез)
                if (fullJson.containsKey("strategyAnalysis")) {
                    context.put("strategyAnalysis", fullJson.get("strategyAnalysis"));
                }
                break;
            case "channels":
                // Новая структура: strategyAnalysis.channelsPotential (List)
                if (fullJson.containsKey("channels")) {
                    context.put("channels", fullJson.get("channels"));
                }
                if (fullJson.containsKey("channelsPotential")) {
                    context.put("channelsPotential", fullJson.get("channelsPotential"));
                } else if (fullJson.containsKey("strategyAnalysis")) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> strategyAnalysis = (Map<String, Object>) fullJson.get("strategyAnalysis");
                    if (strategyAnalysis != null && strategyAnalysis.get("channelsPotential") != null) {
                        Object channelsPotential = strategyAnalysis.get("channelsPotential");
                        context.put("channelsPotential", channelsPotential);
                        // Alias: чтобы текущие промпты (про channels) работали даже без старого ключа
                        if (!context.containsKey("channels")) {
                            context.put("channels", channelsPotential);
                        }
                    }
                }
                // Для канальной матрицы (heatmap) нужен аудиторийный контекст (segments +
                // segmentsChannelMatrix)
                if (fullJson.containsKey("audienceAnalysis")) {
                    context.put("audienceAnalysis", fullJson.get("audienceAnalysis"));
                }
                // Даже если channelsPotential отсутствует/пустой — даём опорный контекст (для
                // гипотез)
                if (fullJson.containsKey("strategyAnalysis")) {
                    context.put("strategyAnalysis", fullJson.get("strategyAnalysis"));
                }
                break;
            case "funnel":
                // Новая структура: strategyAnalysis.funnel (Map со stages)
                if (fullJson.containsKey("funnel")) {
                    context.put("funnel", fullJson.get("funnel"));
                } else if (fullJson.containsKey("strategyAnalysis")) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> strategyAnalysis = (Map<String, Object>) fullJson.get("strategyAnalysis");
                    if (strategyAnalysis != null && strategyAnalysis.get("funnel") != null) {
                        context.put("funnel", strategyAnalysis.get("funnel"));
                    }
                }
                if (fullJson.containsKey("conversionFunnel")) {
                    context.put("conversionFunnel", fullJson.get("conversionFunnel"));
                }
                // Alias: если conversionFunnel отсутствует в полном JSON, но есть
                // strategyAnalysis.funnel.stages —
                // собираем conversionFunnel в формате, ожидаемом графиком [[CHART_FUNNEL]].
                if (!context.containsKey("conversionFunnel") && fullJson.containsKey("strategyAnalysis")) {
                    try {
                        @SuppressWarnings("unchecked")
                        Map<String, Object> strategyAnalysis = (Map<String, Object>) fullJson.get("strategyAnalysis");
                        if (strategyAnalysis != null && strategyAnalysis.get("funnel") instanceof Map) {
                            @SuppressWarnings("unchecked")
                            Map<String, Object> funnel = (Map<String, Object>) strategyAnalysis.get("funnel");
                            if (funnel != null && funnel.get("stages") instanceof List) {
                                @SuppressWarnings("unchecked")
                                List<Map<String, Object>> stages = (List<Map<String, Object>>) funnel.get("stages");
                                if (stages != null && !stages.isEmpty()) {
                                    List<Map<String, Object>> funnelList = new ArrayList<>();
                                    for (Map<String, Object> stage : stages) {
                                        Map<String, Object> stageData = new HashMap<>();
                                        stageData.put("stage", stage.getOrDefault("stageName", "Этап"));
                                        stageData.put("value", stage.getOrDefault("value", 0));
                                        Object conversionObj = stage.get("conversionPercent");
                                        if (conversionObj instanceof Number) {
                                            stageData.put("conversion", ((Number) conversionObj).intValue());
                                        } else {
                                            stageData.put("conversion", 0);
                                        }
                                        funnelList.add(stageData);
                                    }
                                    if (!funnelList.isEmpty()) {
                                        context.put("conversionFunnel", funnelList);
                                    }
                                }
                            }
                        }
                    } catch (Exception ignored) {
                        // Safe fallback: do not fail section context preparation
                    }
                }
                // Даже если funnel отсутствует/пустой — даём опорный контекст (для гипотез)
                if (fullJson.containsKey("strategyAnalysis")) {
                    context.put("strategyAnalysis", fullJson.get("strategyAnalysis"));
                }
                break;
            case "positioning":
                if (fullJson.containsKey("positioning")) {
                    context.put("positioning", fullJson.get("positioning"));
                }
                if (fullJson.containsKey("valueProposition")) {
                    context.put("valueProposition", fullJson.get("valueProposition"));
                }
                // Если явных полей позиционирования нет — всё равно даём опорный контекст для
                // гипотез,
                // но модель должна помечать это как гипотезы (см. промпт секции IX).
                if (fullJson.containsKey("audienceAnalysis")) {
                    context.put("audienceAnalysis", fullJson.get("audienceAnalysis"));
                }
                if (fullJson.containsKey("competitorAnalysis")) {
                    context.put("competitorAnalysis", fullJson.get("competitorAnalysis"));
                }
                if (fullJson.containsKey("market")) {
                    context.put("market", fullJson.get("market"));
                }
                if (fullJson.containsKey("seasonality")) {
                    context.put("seasonality", fullJson.get("seasonality"));
                }
                break;
            case "content":
                // Если contentRecommendations отсутствует, всё равно можно построить
                // контент-стратегию
                // на основе аудитории/воронки/каналов/сезонности/конкурентов.
                if (fullJson.containsKey("contentRecommendations")) {
                    context.put("contentRecommendations", fullJson.get("contentRecommendations"));
                }
                if (fullJson.containsKey("audienceAnalysis")) {
                    context.put("audienceAnalysis", fullJson.get("audienceAnalysis"));
                }
                if (fullJson.containsKey("competitorAnalysis")) {
                    context.put("competitorAnalysis", fullJson.get("competitorAnalysis"));
                }
                if (fullJson.containsKey("seasonality")) {
                    context.put("seasonality", fullJson.get("seasonality"));
                }
                if (fullJson.containsKey("market")) {
                    context.put("market", fullJson.get("market"));
                }
                if (fullJson.containsKey("strategyAnalysis")) {
                    context.put("strategyAnalysis", fullJson.get("strategyAnalysis"));
                }
                break;
            case "strategy":
                // Для итоговой стратегии включаем все ключевые данные
                // Новая структура: strategyAnalysis (funnel, channelsPotential, globalSwot,
                // etc.)
                if (fullJson.containsKey("strategyAnalysis")) {
                    context.put("strategyAnalysis", fullJson.get("strategyAnalysis"));
                } else {
                    // Backward compatibility (старая структура)
                    if (fullJson.containsKey("swot")) {
                        context.put("swot", fullJson.get("swot"));
                    }
                    if (fullJson.containsKey("channels")) {
                        context.put("channels", fullJson.get("channels"));
                    }
                    if (fullJson.containsKey("funnel")) {
                        context.put("funnel", fullJson.get("funnel"));
                    }
                }
                // Дополнительные "склейки" стратегии (небольшие поля)
                if (fullJson.containsKey("positioning")) {
                    context.put("positioning", fullJson.get("positioning"));
                }
                if (fullJson.containsKey("valueProposition")) {
                    context.put("valueProposition", fullJson.get("valueProposition"));
                }
                if (fullJson.containsKey("contentRecommendations")) {
                    context.put("contentRecommendations", fullJson.get("contentRecommendations"));
                }
                break;
        }

        return context;
    }

    /**
     * Генерирует раздел I: Краткое резюме
     */
    private String genSectionSummary(Map<String, Object> contextJson) {
        try {
            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder
                    .append("На основе следующего JSON напиши РАЗДЕЛ I (Краткое резюме) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append(
                    "ВНИМАНИЕ: Ты ОБЯЗАН вставить строку плейсхолдера [[CHART_...]] (или [[TABLE_...]]) строго ВНУТРИ текста раздела, сразу после абзаца, который описывает эти данные. НЕ вставляй это в начало или конец. Это критично для верстки.\n\n");
            promptBuilder.append("1. Объем: минимум 500-700 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
            promptBuilder.append(
                    "5. Рынок: обязательно упомяни оценку объема рынка и темпы роста ТОЛЬКО из JSON (market.marketSize и market.growthRate).\n");
            promptBuilder.append(
                    "   Запрещено использовать фиксированные «заглушки» или одинаковые числа для разных ниш. Если точных данных нет — делай аналитическую экстраполяцию и помечай как \"Экстраполяция\".\n\n");
            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("I. Краткое резюме\n\n");
            promptBuilder.append("Опиши:\n");
            promptBuilder.append("- Что за бизнес (ниша, продукт/услуга)\n");
            promptBuilder.append("- Какую цель преследует бизнес на 6-12 месяцев\n");
            promptBuilder.append("- Ключевые выводы из анализа (3-4 главных инсайта)\n");
            promptBuilder.append("- Общую оценку потенциала и перспектив\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder.append("- Начинай сразу с заголовка \"I. Краткое резюме\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder.append("- Пиши так, чтобы отчет был полезен для принятия бизнес-решений\n\n");
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            appendDetailLevelDirectives(promptBuilder, detailLevel, "summary");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    miniModelName, null, 1, "summary");

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionSummary: AI returned invalid or refusal response");
                return "I. Краткое резюме\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionSummary: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionSummary: Error generating section: {}", e.getMessage(), e);
            return "I. Краткое резюме\n\nОшибка при генерации раздела: " + e.getMessage();
        }
    }

    /**
     * Генерирует раздел II: Анализ продукта
     */
    private String genSectionProduct(Map<String, Object> contextJson) {
        try {
            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder
                    .append("На основе следующего JSON напиши РАЗДЕЛ II (Анализ продукта) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append(
                    "ВНИМАНИЕ: Ты ОБЯЗАН вставить строку плейсхолдера [[CHART_...]] (или [[TABLE_...]]) строго ВНУТРИ текста раздела, сразу после абзаца, который описывает эти данные. НЕ вставляй это в начало или конец. Это критично для верстки.\n\n");
            promptBuilder.append("1. Объем: минимум 500-700 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
            promptBuilder.append(
                    "5. Обязательно назови объем рынка и темпы роста ТОЛЬКО из JSON (market.marketSize и market.growthRate), с краткой ссылкой на источник, если он указан в researchPack/market.sources.\n");
            promptBuilder.append(
                    "6. Объясни, что эти цифры значат для окупаемости фитнес-центра (логика окупаемости/скорость набора клиентов/масштаб спроса),\n");
            promptBuilder.append(
                    "   но НЕ придумывай новые финансовые цифры: если в JSON нет чисел для части расчета — используй качественные выводы + аналитическую экстраполяцию (пометь как \"Экстраполяция\").\n\n");
            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("II. Анализ продукта\n\n");
            promptBuilder.append("Детально проанализируй:\n");
            promptBuilder.append("- Особенности продукта/услуги, его уникальные характеристики\n");
            promptBuilder.append("- Позиционирование на рынке\n");
            promptBuilder.append("- Уникальное торговое предложение (УТП)\n");
            promptBuilder.append("- Тренды в нише и как продукт им соответствует\n");
            promptBuilder.append("- Преимущества и недостатки продукта\n");
            promptBuilder.append("- Потенциал для развития и масштабирования\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder.append("- Начинай сразу с заголовка \"II. Анализ продукта\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder.append("- Используй данные из полей product, businessNiche, positioning, valueProposition\n");
            promptBuilder.append("- Пиши максимально подробно, анализируй тренды\n\n");
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            appendDetailLevelDirectives(promptBuilder, detailLevel, "product");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    miniModelName, null, 1, "product");

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionProduct: AI returned invalid or refusal response");
                return "II. Анализ продукта\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionProduct: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionProduct: Error generating section: {}", e.getMessage(), e);
            return "II. Анализ продукта\n\nОшибка при генерации раздела: " + e.getMessage();
        }
    }

    /**
     * Генерирует раздел III: Анализ рынка и сезонности
     */
    private String genSectionMarket(Map<String, Object> contextJson) {
        try {
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            if (detailLevel == DetailLevel.ПОДРОБНО) {
                Map<String, Object> notes = buildResearchNotesForSection(contextJson, "market");
                if (notes != null && !notes.isEmpty()) {
                    contextJson.put("researchNotes", Map.of("market", notes));
                }
            }
            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder.append(
                    "На основе следующего JSON напиши РАЗДЕЛ III (Анализ рынка и сезонности) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append(
                    "ВНИМАНИЕ: Ты ОБЯЗАН вставить строку плейсхолдера [[CHART_...]] (или [[TABLE_...]]) строго ВНУТРИ текста раздела, сразу после абзаца, который описывает эти данные. НЕ вставляй это в начало или конец. Это критично для верстки.\n\n");
            promptBuilder.append("1. Объем: минимум 500-700 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n");
            promptBuilder.append(
                    "5. Контроль галлюцинаций: если используешь данные из researchPack/market.sources/researchNotes.citations — добавляй в тексте краткую ссылку (в скобках).\n");
            promptBuilder.append(
                    "6. Если данных нет — используй аналитическую экстраполяцию и явно помечай блок как \"Экстраполяция\".\n\n");
            promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРОКА:\n");
            promptBuilder.append(
                    "- Обязательно вставь тег [[CHART_SEASONALITY]] непосредственно рядом с описанием сезонности (внутри абзаца или сразу после него).\n\n");
            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("III. Анализ рынка и сезонности\n\n");
            promptBuilder.append("Детально проанализируй:\n");
            promptBuilder.append("- Рынок Казахстана в данной нише, его размер и динамику\n");
            promptBuilder.append("- Макроэкономические факторы, влияющие на нишу\n");
            promptBuilder.append("- Тренды и изменения в отрасли\n");
            promptBuilder.append("- Сезонные колебания спроса (используй данные из поля seasonality)\n");
            promptBuilder.append("- Как сезонность влияет на кэшфлоу бизнеса\n");
            promptBuilder.append("- Конкретные рекомендации по работе с сезонностью (акции, планирование)\n\n");

            promptBuilder.append("ЖЕСТКОЕ УСЛОВИЕ (SEASONALITY):\n");
            promptBuilder.append(
                    "- Если в JSON присутствуют непустые данные seasonality, ты ОБЯЗАН вставить тег [[CHART_SEASONALITY]] и проанализировать его.\n");
            promptBuilder.append(
                    "- ЗАПРЕЩЕНО писать, что данных нет, если seasonality в JSON не пустой.\n\n");

            promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
            promptBuilder.append(
                    "1. В логичном месте раздела (сразу после конкретного описания сезонных колебаний/пиковых месяцев) вставь тег [[CHART_SEASONALITY]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
            promptBuilder.append(
                    "2. ПЕРЕД тегом напиши вводный абзац (2-3 предложения), но ТОЛЬКО если ты действительно вставляешь тег [[CHART_SEASONALITY]].\n");
            promptBuilder.append(
                    "3. ПОСЛЕ плейсхолдера напиши интерпретацию данных (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_SEASONALITY]].\n\n");
            promptBuilder.append(
                    "ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил тег [[CHART_SEASONALITY]].\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder.append("- Начинай сразу с заголовка \"III. Анализ рынка и сезонности\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder.append("- Используй данные из полей seasonality, market, regions\n");
            promptBuilder.append("- Пиши максимально подробно, анализируй тренды\n");
            promptBuilder.append(
                    "- Каждое ключевое утверждение подтверждай конкретной цифрой из JSON; если нужна связующая метрика — делай экстраполяцию и помечай как \"Экстраполяция\".\n\n");
            appendDetailLevelDirectives(promptBuilder, detailLevel, "market");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            // Для сложных разделов используем ТОЛЬКО textModelName (без fallback на mini)
            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    textModelName, null, 2, "market");

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionMarket: AI returned invalid or refusal response");
                return "III. Анализ рынка и сезонности\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionMarket: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionMarket: Error generating section: {}", e.getMessage(), e);
            return "III. Анализ рынка и сезонности\n\nОшибка при генерации раздела: " + e.getMessage();
        }
    }

    /**
     * Генерирует раздел IV: Анализ целевой аудитории
     */
    private String genSectionAudience(Map<String, Object> contextJson) {
        try {
            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            promptBuilder.append(
                    "КРИТИЧЕСКАЯ ИНСТРУКЦИЯ: Данные в JSON ЕСТЬ. Если ты скажешь, что данных нет — это будет ошибкой. "
                            + "Ты обязан найти объект `audienceAnalysis` в переданном тебе тексте. "
                            + "Обязательно вставь теги [[CHART_AGE_DISTRIBUTION]] и [[CHART_GENDER_DISTRIBUTION]]. "
                            + "ЗАПРЕЩЕНО писать фразу \"К сожалению, данных нет\" (и любые вариации).\n\n");
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder.append(
                    "На основе следующего JSON напиши РАЗДЕЛ IV (Анализ целевой аудитории) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append(
                    "ВНИМАНИЕ: Ты ОБЯЗАН вставить строку плейсхолдера [[CHART_...]] (или [[TABLE_...]]) строго ВНУТРИ текста раздела, сразу после абзаца, который описывает эти данные. НЕ вставляй это в начало или конец. Это критично для верстки.\n\n");
            promptBuilder.append("1. Объем: минимум 500-700 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
            promptBuilder.append(
                    "5. ЦИФРЫ ОБЯЗАТЕЛЬНЫ: в каждом подпункте делай выводы на основе процентов/чисел из audienceAnalysis.ageGroups и audienceAnalysis.genderDistribution.\n\n");
            promptBuilder.append("ОБЯЗАТЕЛЬНЫЕ СТРОКИ:\n");
            promptBuilder.append(
                    "- Обязательно вставь теги [[CHART_AGE_DISTRIBUTION]] и [[CHART_GENDER_DISTRIBUTION]] рядом с описанием конкретных процентов/выводов (внутри абзаца или сразу после него).\n\n");

            promptBuilder.append("ЕСЛИ В JSON НЕТ ТОЧНЫХ ЦИФР (РАЗРЕШЕННАЯ АППРОКСИМАЦИЯ):\n");
            promptBuilder.append(
                    "- Если audienceAnalysis.ageGroups или audienceAnalysis.genderDistribution отсутствуют/пустые: составь реалистичное распределение по нескольким категориям (НЕ 100% в одну).\n");
            promptBuilder.append(
                    "- Если audienceAnalysis.ageGroups присутствует, но все значения кроме одного равны 0 (вырожденное распределение): придумай реалистичное распределение по shown нескольким возрастным группам (например, 25–34 — 40%, 35–44 — 30%, 18–24 — 15%, 45–54 — 10%, 55+ — 5%), чтобы график был наглядным. Обязательно пометь это как \"Экспертная аппроксимация\".\n");
            promptBuilder.append(
                    "- Сумма долей должна быть 100%. Явно пометь это как \"Экспертная аппроксимация\".\n\n");

            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("IV. Анализ целевой аудитории\n\n");
            promptBuilder.append(
                    "Детально проанализируй аудиторию на основе audienceAnalysis (segments, ageGroups, genderDistribution):\n");
            promptBuilder.append(
                    "- Не просто опиши сегменты, а проанализируй данные графиков [[CHART_AGE_DISTRIBUTION]] и [[CHART_GENDER_DISTRIBUTION]].\n");
            promptBuilder.append(
                    "- Обязательно вставь тег [[CHART_AGE_DISTRIBUTION]] после анализа возраста и [[CHART_GENDER_DISTRIBUTION]] после анализа пола. Опиши данные этих графиков.\n");
            promptBuilder.append(
                    "- Объясни, почему именно эти группы принесут больше денег (через частоту, удержание, вероятность апсейла, LTV).\n");
            promptBuilder.append(
                    "- Если финансовые метрики не представлены явно: выполни аналитическую экстраполяцию (LTV/AvgCheck/частота) и опиши методику, пометив блок как \"Экстраполяция\".\n");
            promptBuilder.append("- Демографические характеристики (возраст, пол, доход)\n");
            promptBuilder.append("- Психографический портрет (потребности, мотивация, ценности)\n");
            promptBuilder.append(
                    "- Customer Journey Map для каждого сегмента (этапы от первого контакта до покупки и лояльности)\n");
            promptBuilder.append("- Ключевые потребности и боли сегмента\n");
            promptBuilder.append("- Как продукт решает проблемы каждого сегмента\n");
            promptBuilder.append("- Рекомендации по коммуникации с каждым сегментом\n\n");
            promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКОВ (2 шт):\n");
            promptBuilder.append(
                    "1. Сразу после конкретного описания возрастных долей (проценты по возрастным группам) вставь тег [[CHART_AGE_DISTRIBUTION]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
            promptBuilder.append(
                    "   - ПЕРЕД тегом: вводный абзац (2-3 предложения), но ТОЛЬКО если ты действительно вставляешь тег [[CHART_AGE_DISTRIBUTION]].\n");
            promptBuilder.append(
                    "   - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_AGE_DISTRIBUTION]].\n");
            promptBuilder.append(
                    "2. Сразу после конкретного описания гендерных долей (проценты) вставь тег [[CHART_GENDER_DISTRIBUTION]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
            promptBuilder.append(
                    "   - ПЕРЕД тегом: вводный абзац (2-3 предложения), но ТОЛЬКО если ты действительно вставляешь тег [[CHART_GENDER_DISTRIBUTION]].\n");
            promptBuilder.append(
                    "   - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_GENDER_DISTRIBUTION]].\n\n");
            promptBuilder.append(
                    "ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил соответствующий тег графика.\n\n");
            promptBuilder.append(
                    "ОБЯЗАТЕЛЬНО ПРО ГЕНДЕР:\n- Явно укажи долю женщин по данным genderDistribution (в типовом кейсе около 60%) и объясни, почему аудитория смещена в сторону женщин.\n");
            promptBuilder.append(
                    "- Дай практические рекомендации, как использовать это: месседжи, офферы, визуалы, каналы, время коммуникации.\n\n");
            promptBuilder.append(
                    "ОБЯЗАТЕЛЬНО ПРО ДЕНЬГИ:\n- После каждого крупного вывода добавляй мини-блок (1-2 предложения): \"Как это влияет на выручку\".\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder.append("- Начинай сразу с заголовка \"IV. Анализ целевой аудитории\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder
                    .append("- Используй данные из поля audienceAnalysis (и только из него), не выдумывай проценты\n");
            promptBuilder.append(
                    "- Для каждого сегмента создай детальный портрет клиента и привяжи его к возрастным и гендерным долям\n\n");
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            if (detailLevel == DetailLevel.ПОДРОБНО) {
                promptBuilder.append("ДОПОЛНИТЕЛЬНО ДЛЯ ПОДРОБНОГО ОТЧЕТА (ОБЯЗАТЕЛЬНО):\n");
                promptBuilder.append(
                        "- Для КАЖДОГО сегмента (audienceAnalysis.segments) распиши «День из жизни» клиента (Day in the Life): утро/день/вечер, задачи, триггеры, контекст, боли, мотивация, где и как он сталкивается с нашим продуктом.\n");
                promptBuilder.append(
                        "- В «Day in the Life» явно отметь ключевые точки принятия решения, контент/офферы для каждого касания и тактические рекомендации по коммуникации.\n\n");
            }
            appendDetailLevelDirectives(promptBuilder, detailLevel, "audience");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            // Для сложных разделов используем ТОЛЬКО textModelName (без fallback на mini)
            // Увеличенный таймаут для тяжелой секции (800-1100 слов): 180 секунд
            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    textModelName, null, 2, "audience", 180000L);

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionAudience: AI returned invalid or refusal response");
                return "IV. Анализ целевой аудитории\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionAudience: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionAudience: Error generating section: {}", e.getMessage(), e);
            return "IV. Анализ целевой аудитории\n\nОшибка при генерации раздела: " + e.getMessage();
        }
    }

    /**
     * Генерирует раздел V: Анализ конкурентов
     */
    private String genSectionCompetitors(Map<String, Object> contextJson) {
        try {
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            if (detailLevel == DetailLevel.ПОДРОБНО) {
                Map<String, Object> notes = buildResearchNotesForSection(contextJson, "competitors");
                if (notes != null && !notes.isEmpty()) {
                    contextJson.put("researchNotes", Map.of("competitors", notes));
                }
            }
            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            promptBuilder.append(
                    "КРИТИЧЕСКАЯ ИНСТРУКЦИЯ: Данные в JSON ЕСТЬ. Если ты скажешь, что данных нет — это будет ошибкой. "
                            + "Ты обязан найти объект `competitorAnalysis` в переданном тебе тексте. "
                            + "Обязательно вставь теги [[CHART_MARKET_SHARE]] и [[TABLE_COMPARISON]]. "
                            + "ЗАПРЕЩЕНО писать фразу \"К сожалению, данных нет\" (и любые вариации).\n\n");
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder.append(
                    "На основе следующего JSON напиши РАЗДЕЛ V (Анализ конкурентов) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append(
                    "ВНИМАНИЕ: Ты ОБЯЗАН вставить строку плейсхолдера [[CHART_...]] (или [[TABLE_...]]) строго ВНУТРИ текста раздела, сразу после абзаца, который описывает эти данные. НЕ вставляй это в начало или конец. Это критично для верстки.\n\n");
            promptBuilder.append("1. Объем: минимум 800-1100 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
            promptBuilder.append(
                    "5. Это должен быть самый мощный раздел отчёта: каждое утверждение подтверждай цифрами из competitorAnalysis.marketShareChart / competitorAnalysis.comparisonTable.\n\n");
            promptBuilder.append(
                    "6. Контроль галлюцинаций: если используешь данные из competitorAnalysis.sources/researchPack/researchNotes.citations — добавляй в тексте краткую ссылку (в скобках).\n");
            promptBuilder.append("ОБЯЗАТЕЛЬНЫЕ СТРОКИ:\n");
            promptBuilder.append(
                    "- Обязательно вставь теги [[CHART_MARKET_SHARE]] и [[TABLE_COMPARISON]] рядом с теми утверждениями, где ты обсуждаешь конкретные цифры/сравнения (внутри абзаца или сразу после него).\n\n");
            promptBuilder.append(
                    "КРИТИЧНО (ОБЯЗАТЕЛЬНЫЕ ТЕГИ): Ты ОБЯЗАН вставить тег [[CHART_MARKET_SHARE]] в начале раздела и [[TABLE_COMPARISON]] в середине. Без этих тегов отчет будет неполным.\n");
            promptBuilder.append(
                    "КРИТИЧНО (ТАБЛИЦА): Опиши каждую строку сравнительной таблицы (каждый feature) и сделай практический вывод.\n\n");
            promptBuilder.append("6. Сделай этот раздел самым длинным во всём отчёте.\n");
            promptBuilder.append(
                    "7. Проведи детальный баттл: Наш бренд vs Конкурент А vs Конкурент B. Используй данные из [[TABLE_COMPARISON]].\n");
            promptBuilder.append(
                    "8. Ты ОБЯЗАН сравнить наш бренд с Конкурентом A по КАЖДОЙ строке таблицы и сделать вывод (где мы сильнее/слабее, чем это монетизировать).\n");
            promptBuilder.append(
                    "9. РАСЧЕТ (ОБЯЗАТЕЛЬНО): рассчитай разрыв в охвате в процентах относительно НАС: $$ gap\\% = \\frac{Reach_{A}-Reach_{Us}}{Reach_{Us}}\\times 100\\% $$. Подставь числа из competitorAnalysis.competitorsDetails.digitalMetrics.reach (или ближайший числовой прокси из JSON) и интерпретируй бизнес-эффект.\n\n");
            promptBuilder.append(
                    "10. Проанализируй график долей рынка [[CHART_MARKET_SHARE]] и предложи стратегию по \"откусыванию\" доли у лидера (что именно меняем: оффер/цена/продукт/каналы/контент).\n\n");
            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("V. Анализ конкурентов\n\n");
            promptBuilder.append("Детально проанализируй:\n");
            promptBuilder.append("- Позиции каждого конкурента на рынке (используй данные из competitorAnalysis)\n");
            promptBuilder.append("- Анализ долей рынка (используй competitorAnalysis.marketShareChart)\n");
            promptBuilder.append(
                    "- Сильные и слабые стороны конкурентов (из competitorAnalysis.comparisonTable и swotCompetitors)\n");
            promptBuilder.append("- Стратегии конкурентов (ценовая политика, каналы продвижения)\n");
            promptBuilder.append(
                    "- Контент-воронки конкурентов: какие типы контента они используют на каждом этапе воронки, какие каналы задействуют, какие месседжи доносят\n");
            promptBuilder.append(
                    "- Глубокое сравнение нашего бренда с конкурентами (используй competitorAnalysis.comparisonTable)\n");
            promptBuilder.append("- Рекомендации по дифференциации и конкурентным преимуществам\n\n");
            promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА И ТАБЛИЦЫ:\n");
            promptBuilder.append(
                    "1. В НАЧАЛЕ раздела (сразу после первого абзаца с выводом по долям рынка) вставь тег [[CHART_MARKET_SHARE]] рядом с этим выводом (внутри абзаца или сразу после него)\n");
            promptBuilder.append(
                    "   - ПЕРЕД тегом: вводный абзац (2-3 предложения), но ТОЛЬКО если ты действительно вставляешь тег [[CHART_MARKET_SHARE]].\n");
            promptBuilder.append(
                    "   - ПОСЛЕ плейсхолдера: интерпретация (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_MARKET_SHARE]].\n");
            promptBuilder.append(
                    "2. В СЕРЕДИНЕ раздела, сразу после абзаца где ты перечисляешь ключевые различия/feature-ы, вставь тег [[TABLE_COMPARISON]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
            promptBuilder.append(
                    "   - ПЕРЕД таблицей: короткий ввод (1-2 предложения), что именно сравниваем\n");
            promptBuilder.append(
                    "   - ПОСЛЕ таблицы: интерпретация (3-5 предложений) с выводом по позиционированию\n\n");
            promptBuilder.append(
                    "ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил тег [[CHART_MARKET_SHARE]].\n\n");
            promptBuilder.append(
                    "ОБЯЗАТЕЛЬНОЕ СРАВНЕНИЕ С 'КОНКУРЕНТОМ A' И 'КОНКУРЕНТОМ B':\n");
            promptBuilder.append(
                    "- Возьми двух главных конкурентов из competitorAnalysis (по доле рынка или по описанию) и назови их в тексте как \"Конкурент A\" и \"Конкурент B\".\n");
            promptBuilder.append(
                    "- Пройди ПО КАЖДОЙ строке competitorAnalysis.comparisonTable: Мы vs Конкурент A vs Конкурент B (с цифрами/формулировками из JSON) + практический вывод.\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder.append("- Начинай сразу с заголовка \"V. Анализ конкурентов\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder.append(
                    "- Используй данные из competitorAnalysis.marketShareChart и competitorAnalysis.comparisonTable\n");
            promptBuilder.append(
                    "- Не выдумывай цифры. Если в конкретной строке/метрике нет явного значения — делай консервативную экстраполяцию и помечай как \"Экстраполяция\" (без фраз про отсутствие данных).\n\n");
            appendDetailLevelDirectives(promptBuilder, detailLevel, "competitors");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            // Для сложных разделов используем ТОЛЬКО textModelName (без fallback на mini)
            // Увеличенный таймаут для тяжелой секции (800-1100 слов): 240 секунд
            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    textModelName, null, 2, "competitors", 240000L);

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionCompetitors: AI returned invalid or refusal response");
                return "V. Анализ конкурентов\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionCompetitors: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionCompetitors: Error generating section: {}", e.getMessage(), e);
            return "V. Анализ конкурентов\n\nОшибка при генерации раздела: " + e.getMessage();
        }
    }

    /**
     * Генерирует раздел VI: SWOT-анализ
     */
    private String genSectionSWOT(Map<String, Object> contextJson) {
        try {
            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder.append("На основе следующего JSON напиши РАЗДЕЛ VI (SWOT-анализ) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append("1. Объем: минимум 500-700 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
            promptBuilder.append("5. РАЗДЕЛ НЕЛЬЗЯ ПРОПУСКАТЬ:\n");
            promptBuilder.append(
                    "   - Если в JSON нет поля swot или оно неполное: сформируй SWOT как ГИПОТЕЗУ на основе product/businessNiche/goal/market/audienceAnalysis/competitorAnalysis/strategyAnalysis.\n");
            promptBuilder.append(
                    "   - Каждый пункт, который не подтвержден напрямую JSON-данными, помечай префиксом \"Гипотеза:\".\n");
            promptBuilder.append(
                    "   - Для каждого блока (S/W/O/T) добавь подпункт \"Какие данные собрать для подтверждения\" (3-5 пунктов).\n\n");
            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("VI. SWOT-анализ\n\n");
            promptBuilder.append("Детально проанализируй:\n");
            promptBuilder.append("- Сильные стороны (Strengths) - как их использовать\n");
            promptBuilder.append("- Слабые стороны (Weaknesses) - как их минимизировать\n");
            promptBuilder.append("- Возможности (Opportunities) - как их реализовать\n");
            promptBuilder.append("- Угрозы (Threats) - как им противостоять\n");
            promptBuilder.append("- КРИТИЧНО: Проведи перекрестный анализ:\n");
            promptBuilder.append("  * Как сильные стороны помогут использовать возможности\n");
            promptBuilder.append("  * Как слабые стороны могут помешать противостоять угрозам\n");
            promptBuilder.append("  * Стратегические комбинации (SO, ST, WO, WT)\n");
            promptBuilder.append("- Практические рекомендации на основе SWOT-матрицы\n\n");
            promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
            promptBuilder.append(
                    "1. Сразу после конкретного описания пунктов SWOT (S/W/O/T) вставь тег [[CHART_SWOT]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
            promptBuilder.append(
                    "2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике показывает...\"\n");
            promptBuilder.append(
                    "3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder.append("- Начинай сразу с заголовка \"VI. SWOT-анализ\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder.append("- Если поле swot присутствует в JSON — используй его как основу.\n");
            promptBuilder.append("- Обязательно проведи перекрестный анализ пересечений\n\n");
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            appendDetailLevelDirectives(promptBuilder, detailLevel, "swot");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    textModelName, miniModelName, 2, "swot");

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionSWOT: AI returned invalid or refusal response");
                return "VI. SWOT-анализ\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionSWOT: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionSWOT: Error generating section: {}", e.getMessage(), e);
            return "VI. SWOT-анализ\n\nОшибка при генерации раздела: " + e.getMessage();
        }
    }

    /**
     * Генерирует раздел VII: Каналы продвижения и их потенциал
     */
    private String genSectionChannels(Map<String, Object> contextJson) {
        try {
            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder.append(
                    "На основе следующего JSON напиши РАЗДЕЛ VII (Каналы продвижения и их потенциал) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append("1. Объем: минимум 600-900 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
            promptBuilder.append("5. РАЗДЕЛ НЕЛЬЗЯ ПРОПУСКАТЬ:\n");
            promptBuilder.append(
                    "   - Если в JSON отсутствуют channels/channelsPotential: составь список каналов как ГИПОТЕЗЫ, опираясь на нишу/регион/цель, аудиторию (audienceAnalysis), сезонность и конкурентов.\n");
            promptBuilder.append(
                    "   - Если по каналу нет чисел: делай консервативную экстраполяцию (помечай как \"Экстраполяция\") и перечисли, какие метрики нужно собрать (CPC/CPM/CTR/CR/CPL/CPA/LTV).\n");
            promptBuilder.append(
                    "   - Если channelsPotential присутствует: обязательно используй potentialIndex/priority/обоснование.\n\n");
            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("VII. Каналы продвижения и их потенциал\n\n");
            promptBuilder.append("Детально проанализируй КАЖДЫЙ канал из channels/channelsPotential (если есть):\n");
            promptBuilder.append("- Потенциал канала для данной ниши (potentialIndex)\n");
            promptBuilder.append("- Приоритетность канала (priorityLevel) и обоснование\n");
            promptBuilder.append("- Роль канала в общей стратегии (roleInStrategy)\n");
            promptBuilder.append("- Обоснование важности канала (justification)\n");
            promptBuilder.append("- Рекомендации по использованию каждого канала\n");
            promptBuilder.append("- Бюджетные рекомендации и ожидаемая эффективность\n");
            promptBuilder.append("- Интеграция каналов в единую маркетинговую стратегию\n\n");
            promptBuilder.append("КРИТИЧНО (ОБЯЗАТЕЛЬНЫЕ ТЕГИ):\n");
            promptBuilder.append(
                    "- Ты ОБЯЗАН вставить [[CHART_CHANNELS]] и [[CHART_CHANNEL_MATRIX]] внутри текста (рядом с выводами по цифрам).\n");
            promptBuilder.append(
                    "- [[CHART_CHANNEL_MATRIX]] относится к segmentsChannelMatrix (heatmap сегмент×канал). Используй её для причинно-следственных выводов.\n\n");
            promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
            promptBuilder.append(
                    "1. Сразу после абзаца, где ты сравниваешь/ранжируешь каналы с конкретными аргументами, вставь тег [[CHART_CHANNELS]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
            promptBuilder.append(
                    "2. ПЕРЕД графиком напиши вводный абзац (2-3 предложения), начинающийся с: \"Анализ данных на графике показывает...\"\n");
            promptBuilder.append(
                    "3. ПОСЛЕ графика напиши интерпретацию данных (2-3 предложения), начинающуюся с: \"Из этих показателей следует, что...\"\n\n");
            promptBuilder.append("ОБЯЗАТЕЛЬНЫЙ АНАЛИЗ ПО МАТРИЦЕ (SEGMENTS×CHANNELS):\n");
            promptBuilder.append(
                    "- Вставь [[CHART_CHANNEL_MATRIX]] рядом с абзацем, где ты объясняешь поведение сегментов в каналах.\n");
            promptBuilder.append(
                    "- ОБЯЗАТЕЛЬНО проанализируй матрицу: объясни, почему для конкретного сегмента (например, \"Молодежь\") один канал имеет приоритет High, а другой Low. Делай выводы ТОЛЬКО с привязкой к конкретным ячейкам segmentsChannelMatrix (значения High/Medium/Low).\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder.append("- Начинай сразу с заголовка \"VII. Каналы продвижения и их потенциал\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder.append("- Используй данные из полей channels, channelsPotential\n");
            promptBuilder.append(
                    "- Используй audienceAnalysis.segmentsChannelMatrix (если есть) для обоснования приоритетов.\n");
            promptBuilder.append("- Для каждого канала дай конкретные рекомендации\n\n");
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            appendDetailLevelDirectives(promptBuilder, detailLevel, "channels");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    textModelName, miniModelName, 2, "channels");

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionChannels: AI returned invalid or refusal response");
                return "VII. Каналы продвижения и их потенциал\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionChannels: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionChannels: Error generating section: {}", e.getMessage(), e);
            return "VII. Каналы продвижения и их потенциал\n\nОшибка при генерации раздела: " + e.getMessage();
        }
    }

    /**
     * Генерирует раздел VIII: Воронка спроса
     */
    private String genSectionFunnel(Map<String, Object> contextJson) {
        try {
            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder
                    .append("На основе следующего JSON напиши РАЗДЕЛ VIII (Воронка спроса) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append(
                    "ВНИМАНИЕ: Ты ОБЯЗАН вставить строку плейсхолдера [[CHART_...]] (или [[TABLE_...]]) строго ВНУТРИ текста раздела, сразу после абзаца, который описывает эти данные. НЕ вставляй это в начало или конец. Это критично для верстки.\n\n");
            promptBuilder.append("1. Объем: минимум 600-900 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n");
            promptBuilder.append("ОБЯЗАТЕЛЬНАЯ СТРОКА:\n");
            promptBuilder.append(
                    "- Обязательно вставь тег [[CHART_FUNNEL]] непосредственно рядом с описанием этапов/конверсий (внутри абзаца или сразу после него).\n\n");
            promptBuilder.append("МАТЕМАТИКА И LaTeX (ОБЯЗАТЕЛЬНО):\n");
            promptBuilder.append(
                    "- Все формулы в этом разделе пиши ТОЛЬКО в LaTeX.\n");
            promptBuilder.append(
                    "- Обязательно вставь фразу: \"Вероятная прибыль рассчитывается по формуле: $$ P = V \\times CR \\times AvgCheck $$\" и используй её в расчетах.\n\n");
            promptBuilder.append(
                    "ВНИМАНИЕ: Проверяй математическую логику. Количество покупок НЕ МОЖЕТ быть больше количества заявок. "
                            + "Конверсия не может превышать 100%. Если данные в JSON противоречивы, используй логические значения "
                            + "(например, конверсия 10%) и сделай пометку 'Требуется уточнение данных'.\n");
            promptBuilder.append("5. РАЗДЕЛ НЕЛЬЗЯ ПРОПУСКАТЬ:\n");
            promptBuilder.append(
                    "   - Если в JSON нет funnel/conversionFunnel: построй воронку как ГИПОТЕЗУ (этапы + логика), но все числа замени на переменные (X1, X2...) и пометь блок как \"Экстраполяция\".\n");
            promptBuilder.append(
                    "   - Обязательно добавь блок: какие данные нужно собрать для расчета (трафик, лиды, заявки, оплаты, CR по этапам, CPL/CPA).\n");
            promptBuilder.append(
                    "   - Если funnel/conversionFunnel присутствуют: делай выводы строго по цифрам из JSON.\n\n");
            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("VIII. Воронка спроса\n\n");
            promptBuilder.append("Детально проанализируй:\n");
            promptBuilder.append("- Каждый этап воронки (stages из funnel): название, значение, конверсия\n");
            promptBuilder.append("- Где находится \"узкое место\" воронки (самая низкая конверсия)\n");
            promptBuilder.append("- Причины низкой конверсии на проблемных этапах\n");
            promptBuilder.append("- Конкретные рекомендации по улучшению каждого этапа\n");
            promptBuilder.append("- Как оптимизировать узкое место для увеличения общего конверсионного потока\n");
            promptBuilder.append("- Ожидаемый эффект от оптимизации (количественные оценки)\n");
            promptBuilder.append("- Интеграция воронки с каналами продвижения\n\n");
            promptBuilder.append("ОБЯЗАТЕЛЬНЫЙ РАСЧЕТ ПО ПЕРЕХОДУ \"Интерес → Заявка\":\n");
            promptBuilder.append(
                    "- Найди этапы \"Интерес\" и \"Заявка\" (или ближайшие по смыслу) и посчитай, сколько человек теряем: $$ Lost = V_{Interest} - V_{Lead} $$. \n");
            promptBuilder.append(
                    "- Оцени денежную стоимость потерь через экстраполяцию (например, Lost × CR_toPurchase × AvgCheck) и интерпретируй управленчески.\n\n");
            promptBuilder.append("КРИТИЧНО - ВСТАВКА ГРАФИКА:\n");
            promptBuilder.append(
                    "1. Сразу после описания этапов и ключевых выводов по узким местам вставь тег [[CHART_FUNNEL]] рядом с этим описанием (внутри абзаца или сразу после него)\n");
            promptBuilder.append(
                    "2. ПЕРЕД тегом напиши вводный абзац (2-3 предложения), но ТОЛЬКО если ты действительно вставляешь тег [[CHART_FUNNEL]].\n");
            promptBuilder.append(
                    "3. ПОСЛЕ плейсхолдера напиши интерпретацию данных (2-3 предложения), но ТОЛЬКО если в тексте присутствует [[CHART_FUNNEL]].\n\n");
            promptBuilder.append(
                    "ЗАПРЕТ НА ЗАГЛУШКИ:\n- Запрещено писать фразы типа \"Анализ данных на графике показывает...\" и \"Из этих показателей следует, что...\", если ты НЕ вставил тег [[CHART_FUNNEL]].\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder.append("- Начинай сразу с заголовка \"VIII. Воронка спроса\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder.append("- Используй данные из полей funnel, conversionFunnel\n");
            promptBuilder.append("- Обязательно укажи, где \"узкое место\" и как его исправить\n\n");
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            appendDetailLevelDirectives(promptBuilder, detailLevel, "funnel");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    textModelName, miniModelName, 2, "funnel");

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionFunnel: AI returned invalid or refusal response");
                return "VIII. Воронка спроса\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionFunnel: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionFunnel: Error generating section: {}", e.getMessage(), e);
            return "VIII. Воронка спроса\n\nОшибка при генерации раздела: " + e.getMessage();
        }
    }

    /**
     * Генерирует раздел IX: Рекомендации по позиционированию и УТП
     */
    private String genSectionPositioning(Map<String, Object> contextJson) {
        try {
            // Этот раздел может быть качественным. Если полей positioning/valueProposition
            // нет,
            // разрешаем только гипотезы + план тестирования, не выдавая их за факты.
            // Не пропускаем раздел: если данных нет, будет гипотезирование + план проверки
            // (см. промпт ниже)

            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder.append(
                    "На основе следующего JSON напиши РАЗДЕЛ IX (Рекомендации по позиционированию и УТП) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append("1. Объем: минимум 500-700 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n");
            promptBuilder.append("ВАЖНО ПРО НЕДОСТАЮЩИЕ ДАННЫЕ:\n");
            promptBuilder.append(
                    "- Если в JSON нет positioning и/или valueProposition: сформулируй 2-3 ГИПОТЕЗЫ позиционирования и УТП.\n");
            promptBuilder.append(
                    "- Каждую гипотезу помечай как \"Гипотеза\" и добавляй план проверки (A/B тест, оффер-тест, опрос), не выдавая её за факт.\n\n");
            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("IX. Рекомендации по позиционированию и УТП\n\n");
            promptBuilder.append("Детально проанализируй:\n");
            promptBuilder.append("- Позиционирование бренда (brandImage, keyMessage, toneOfVoice)\n");
            promptBuilder.append("- Уникальное торговое предложение (УТП) - как его сформулировать и донести\n");
            promptBuilder.append("- Факторы доверия (trustFactors) и как их использовать\n");
            promptBuilder.append("- Как позиционирование дифференцирует от конкурентов\n");
            promptBuilder.append("- Рекомендации по коммуникации позиционирования через разные каналы\n");
            promptBuilder.append("- Как УТП решает проблемы целевой аудитории\n");
            promptBuilder.append("- Практические шаги по внедрению позиционирования\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder.append("- Начинай сразу с заголовка \"IX. Рекомендации по позиционированию и УТП\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder.append("- Используй данные из полей positioning, valueProposition\n");
            promptBuilder.append("- Давай конкретные, практические рекомендации\n\n");
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            appendDetailLevelDirectives(promptBuilder, detailLevel, "positioning");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    miniModelName, textModelName, 2, "positioning");

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionPositioning: AI returned invalid or refusal response");
                return "IX. Рекомендации по позиционированию и УТП\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionPositioning: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionPositioning: Error generating section: {}", e.getMessage(), e);
            return "IX. Рекомендации по позиционированию и УТП\n\nОшибка при генерации раздела: " + e.getMessage();
        }
    }

    /**
     * Генерирует раздел X: Рекомендации по контенту
     */
    private String genSectionContent(Map<String, Object> contextJson) {
        try {
            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder.append(
                    "На основе следующего JSON напиши РАЗДЕЛ X (Рекомендации по контенту) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append("1. Объем: минимум 500-700 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n");
            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("X. Рекомендации по контенту\n\n");
            promptBuilder.append("Детально проанализируй:\n");
            promptBuilder.append(
                    "- Если в JSON есть contentRecommendations: используй их как основу и доработай/структурируй\n");
            promptBuilder.append(
                    "- Если contentRecommendations НЕТ: разработай контент-стратегию, опираясь на audienceAnalysis, strategyAnalysis (funnel/channelsPotential), seasonality и competitorAnalysis\n");
            promptBuilder.append("- Форматы контента для разных каналов продвижения\n");
            promptBuilder.append("- Контент для разных этапов воронки (осведомленность, интерес, решение, действие)\n");
            promptBuilder.append("- Контент для разных сегментов целевой аудитории\n");
            promptBuilder.append("- Рекомендации по календарю контента (частота, темы, сезонность)\n");
            promptBuilder.append("- Как контент поддерживает позиционирование и УТП\n");
            promptBuilder.append("- Метрики эффективности контента и KPI\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder.append("- Начинай сразу с заголовка \"X. Рекомендации по контенту\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder.append("- Используй данные из поля contentRecommendations\n");
            promptBuilder.append("- Давай конкретные, практические рекомендации по типам и форматам контента\n\n");
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            appendDetailLevelDirectives(promptBuilder, detailLevel, "content");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    miniModelName, textModelName, 2, "content");

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionContent: AI returned invalid or refusal response");
                return "X. Рекомендации по контенту\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionContent: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionContent: Error generating section: {}", e.getMessage(), e);
            return "X. Рекомендации по контенту\n\nОшибка при генерации раздела: " + e.getMessage();
        }
    }

    /**
     * Генерирует раздел XI: Итоговая стратегия
     */
    private String genSectionStrategy(Map<String, Object> contextJson) {
        try {
            DetailLevel detailLevel = resolveDetailLevelFromContext(contextJson);
            if (detailLevel == DetailLevel.ПОДРОБНО) {
                Map<String, Object> notes = buildResearchNotesForSection(contextJson, "strategy");
                if (notes != null && !notes.isEmpty()) {
                    contextJson.put("researchNotes", Map.of("strategy", notes));
                }
            }
            String jsonString = objectMapper.writeValueAsString(contextJson);
            StringBuilder promptBuilder = new StringBuilder();
            promptBuilder.append("Ты — высокооплачиваемый бизнес-консультант и маркетинговый аналитик.\n\n");
            appendUltimateJsonPresenceInstruction(promptBuilder);
            appendMandatoryJsonUsageWarning(promptBuilder);
            appendPremiumConsultingStyleGuide(promptBuilder);
            promptBuilder.append(
                    "На основе следующего JSON напиши РАЗДЕЛ XI (Итоговая стратегия: что делать в первую очередь) маркетингового отчета.\n\n");
            promptBuilder.append("ТРЕБОВАНИЯ:\n");
            promptBuilder.append("1. Объем: минимум 500-700 слов\n");
            promptBuilder.append("2. Стиль: деловой, убедительный для владельца бизнеса\n");
            promptBuilder.append("3. Глубина: аналитические инсайты с практическими выводами\n");
            promptBuilder.append("4. Данные: используй ТОЛЬКО данные из JSON, не выдумывай цифры\n\n");
            promptBuilder.append("СТРУКТУРА РАЗДЕЛА:\n");
            promptBuilder.append("XI. Итоговая стратегия: что делать в первую очередь\n\n");
            promptBuilder.append("Создай конкретный план действий на 6-12 месяцев:\n");
            promptBuilder.append("- Приоритетные задачи (что делать в первую очередь, во вторую, в третью)\n");
            promptBuilder.append("- Конкретные шаги с указанием сроков и ответственных\n");
            promptBuilder.append("- Интеграция всех элементов: позиционирование, каналы, контент, воронка\n");
            promptBuilder.append("- Как использовать сильные стороны и возможности (из SWOT)\n");
            promptBuilder.append("- Как минимизировать слабые стороны и угрозы (из SWOT)\n");
            promptBuilder.append("- Ожидаемые результаты и метрики успеха\n");
            promptBuilder.append("- Бюджетные рекомендации и распределение ресурсов\n");
            promptBuilder.append("- Критические точки контроля и корректировки стратегии\n\n");
            promptBuilder.append("КРИТИЧНО - ПРОГНОЗ И РАСЧЕТЫ НА ОСНОВЕ ВОРОНКИ:\n");
            promptBuilder.append("МАТЕМАТИКА И LaTeX (ОБЯЗАТЕЛЬНО):\n");
            promptBuilder.append("- Все формулы в этом разделе пиши ТОЛЬКО в LaTeX.\n");
            promptBuilder.append(
                    "- Обязательно вставь фразу: \"Вероятная прибыль рассчитывается по формуле: $$ P = V \\times CR \\times AvgCheck $$\".\n\n");
            promptBuilder.append("UNIT-ЭКОНОМИКА (ОБЯЗАТЕЛЬНО, LaTeX):\n");
            promptBuilder.append(
                    "- CAC: \\[ CAC = \\frac{Marketing\\ Spend + Sales\\ Spend}{New\\ Customers} \\].\n");
            promptBuilder.append(
                    "- LTV: \\[ LTV = AvgCheck \\times Frequency \\times GrossMargin \\times Lifetime \\].\n");
            promptBuilder.append(
                    "- ROI: \\[ ROI = \\frac{Profit - Investment}{Investment} \\times 100\\% \\].\n");
            promptBuilder.append(
                    "- Подставь числа из JSON (если есть). Если чисел нет — сделай реалистичные допущения и пометь блок как \"Экстраполяция\".\n\n");
            promptBuilder.append(
                    "1. Возьми воронку из strategyAnalysis.funnel (или из funnel, если strategyAnalysis отсутствует) и выполни расчеты.\n");
            promptBuilder.append(
                    "ВНИМАНИЕ: Проверяй математическую логику. Количество покупок НЕ МОЖЕТ быть больше количества заявок. "
                            + "Конверсия не может превышать 100%. Если данные в JSON противоречивы, используй логические значения "
                            + "(например, конверсия 10%) и сделай пометку 'Требуется уточнение данных'.\n");
            promptBuilder.append(
                    "2. Дай минимум 3 прогнозных показателя с формулами и подстановкой чисел из JSON (например: конверсия между этапами, ожидаемое число покупок, требуемый объем верхнего этапа для достижения цели).\n");
            promptBuilder.append(
                    "3. Сделай 3 сценария: консервативный / базовый / оптимистичный (допуски только как проценты к значениям из JSON; не придумывай абсолютные значения).\n");
            promptBuilder.append(
                    "4. Если в JSON нет явного значения для части метрики — делай аккуратную экстраполяцию (пометь как \"Экстраполяция\") и показывай допущения.\n\n");
            promptBuilder.append("ВАЖНО:\n");
            promptBuilder
                    .append("- Начинай сразу с заголовка \"XI. Итоговая стратегия: что делать в первую очередь\"\n");
            promptBuilder.append("- Не добавляй вступления или заключения\n");
            promptBuilder.append(
                    "- Используй данные из strategyAnalysis (и/или swot/channels/funnel при наличии), positioning, valueProposition, contentRecommendations\n");
            promptBuilder.append("- Давай конкретный, практический план действий с приоритетами\n");
            promptBuilder.append("- Учитывай цель бизнеса на 6-12 месяцев (goal)\n\n");
            if (detailLevel == DetailLevel.ПОДРОБНО) {
                promptBuilder.append("ДОПОЛНИТЕЛЬНО ДЛЯ ПОДРОБНОГО ОТЧЕТА (ОБЯЗАТЕЛЬНО):\n");
                promptBuilder.append(
                        "- Составь помесячный план на все 12 месяцев (логика Gantt): перечисли 5–8 workstreams (например: продукт/оффер, перформанс, контент, CRM, продажи, партнерства, аналитика), и для каждого укажи активные месяцы (1..12), зависимости, контрольные точки и KPI.\n");
                promptBuilder.append(
                        "- Представь Gantt-логику в виде компактной таблицы (workstream × месяцы 1..12) + краткие пояснения по зависимостям.\n");
                promptBuilder.append(
                        "- Макро: обязательно учти инфляцию в РК, курс тенге и логистические риски (используй researchPack/researchNotes). Если есть источник — добавь короткую ссылку.\n");
                promptBuilder.append(
                        "- Фреймворки: обязательно включи PESTEL и 5 сил Портера (как отдельные подблоки) и привяжи выводы к практическим действиям.\n\n");
                promptBuilder.append("BREAK-EVEN (ОБЯЗАТЕЛЬНО):\n");
                promptBuilder.append(
                        "- Рассчитай точку безубыточности (Break-even point) строго по формуле в LaTeX: \\[ BEP = \\frac{Fixed\\ Costs}{Price - Variable\\ Costs} \\].\n");
                promptBuilder.append(
                        "- Если в JSON нет нужных чисел: введи реалистичные допущения (пометь как \"Экстраполяция\") и сделай 3 сценария (консервативный/базовый/оптимистичный), затем интерпретируй управленчески.\n\n");
            }
            appendDetailLevelDirectives(promptBuilder, detailLevel, "strategy");
            promptBuilder.append(
                    "КОНТРОЛЬ ГАЛЛЮЦИНАЦИЙ:\n- Если используешь данные из researchPack/market.sources/competitorAnalysis.sources/researchNotes.citations — добавляй в тексте краткую ссылку (в скобках).\n"
                            + "- Если данных нет — используй аналитическую экстраполяцию и явно помечай блок как \"Экстраполяция\".\n\n");
            promptBuilder.append("JSON ДАННЫЕ:\n");
            promptBuilder.append(jsonString);

            // Увеличенный таймаут для тяжелой секции (800-1100 слов): 240 секунд
            String result = generateWithRetryForSection(contextJson, jsonString, promptBuilder.toString(), "ru",
                    textModelName, null, 2, "strategy", 240000L);

            if (!isValidAiResponse(result)) {
                logger.warn("genSectionStrategy: AI returned invalid or refusal response");
                return "XI. Итоговая стратегия: что делать в первую очередь\n\nНе удалось сгенерировать раздел.";
            }

            logger.info("genSectionStrategy: Section generated successfully. Length: {} chars", result.length());
            return result;
        } catch (Exception e) {
            logger.error("genSectionStrategy: Error generating section: {}", e.getMessage(), e);
            return "XI. Итоговая стратегия: что делать в первую очередь\n\nОшибка при генерации раздела: "
                    + e.getMessage();
        }
    }

    // Удалены неиспользуемые stage-генераторы (Stage 1-4) и
    // validateJsonAnalysis(...).
    // Используется модульная генерация разделов I–XI (genSection*).

    /**
     * Построение строки контекста из запроса
     */
    private String buildContextString(MarketingAnalysisRequest request) {
        StringBuilder contextBuilder = new StringBuilder();
        contextBuilder.append("Ниша бизнеса: ").append(request.getBusinessNiche()).append("\n");
        contextBuilder.append("Продукт/услуга: ").append(request.getProduct()).append("\n");

        Map<String, Object> targetAudienceData = request.getTargetAudience();
        if (targetAudienceData != null) {
            StringBuilder audienceBuilder = new StringBuilder();
            if (targetAudienceData.containsKey("genders")) {
                @SuppressWarnings("unchecked")
                List<String> genders = (List<String>) targetAudienceData.get("genders");
                if (genders != null && !genders.isEmpty()) {
                    audienceBuilder.append("Гендер: ").append(String.join(", ", genders)).append(". ");
                }
            }
            if (targetAudienceData.containsKey("ageRanges")) {
                @SuppressWarnings("unchecked")
                List<String> ageRanges = (List<String>) targetAudienceData.get("ageRanges");
                if (ageRanges != null && !ageRanges.isEmpty()) {
                    audienceBuilder.append("Возраст: ").append(String.join(", ", ageRanges)).append(". ");
                }
            }
            if (targetAudienceData.containsKey("types")) {
                @SuppressWarnings("unchecked")
                List<String> types = (List<String>) targetAudienceData.get("types");
                if (types != null && !types.isEmpty()) {
                    audienceBuilder.append("Тип: ").append(String.join(", ", types)).append(". ");
                }
            }
            contextBuilder.append("Целевая аудитория: ").append(audienceBuilder.toString().trim()).append("\n");
        }

        List<String> regions = request.getRegion();
        if (regions != null && !regions.isEmpty()) {
            contextBuilder.append("Регион: ").append(String.join(", ", regions)).append("\n");
        }
        contextBuilder.append("Цель на 6-12 месяцев: ").append(request.getGoal()).append("\n");
        if (request.getStrongSide() != null && !request.getStrongSide().trim().isEmpty()) {
            contextBuilder.append("Сильная сторона: ").append(request.getStrongSide()).append("\n");
        }
        if (request.getWeakSide() != null && !request.getWeakSide().trim().isEmpty()) {
            contextBuilder.append("Слабая сторона: ").append(request.getWeakSide()).append("\n");
        }

        return contextBuilder.toString();
    }

    /**
     * Парсинг данных для графиков из JSON-анализа
     * Обновлено для работы с новой структурой: audienceAnalysis,
     * competitorAnalysis, seasonality, market
     */
    private Map<String, Object> parseChartDataFromJson(Map<String, Object> analysisJson) {
        logger.info("parseChartDataFromJson: Starting chart data parsing");
        Map<String, Object> chartsData = new HashMap<>();

        if (analysisJson == null || analysisJson.isEmpty()) {
            logger.warn("parseChartDataFromJson: analysisJson is null or empty");
            return chartsData;
        }
        logger.debug("parseChartDataFromJson: AnalysisJson keys: {}", analysisJson.keySet());

        try {
            // 1. Сезонность (структура не изменилась)
            @SuppressWarnings("unchecked")
            Map<String, Object> seasonality = (Map<String, Object>) analysisJson.get("seasonality");
            if (seasonality != null) {
                @SuppressWarnings("unchecked")
                List<String> months = (List<String>) seasonality.get("months");
                @SuppressWarnings("unchecked")
                List<Object> demandIndex = (List<Object>) seasonality.get("demandIndex");

                if (months != null && demandIndex != null && months.size() == demandIndex.size()) {
                    Map<String, Integer> seasonalityMap = new LinkedHashMap<>();
                    for (int i = 0; i < months.size() && i < demandIndex.size(); i++) {
                        String month = months.get(i);
                        Object valueObj = demandIndex.get(i);
                        if (valueObj instanceof Number) {
                            seasonalityMap.put(month, ((Number) valueObj).intValue());
                        }
                    }
                    if (!seasonalityMap.isEmpty()) {
                        chartsData.put("seasonality", seasonalityMap);
                    }
                }
            }

            // 2. Конкуренты (новая структура: competitorAnalysis)
            @SuppressWarnings("unchecked")
            Map<String, Object> competitorAnalysis = (Map<String, Object>) analysisJson.get("competitorAnalysis");
            if (competitorAnalysis != null) {
                // 2.1. Market Share Chart
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> marketShareChart = (List<Map<String, Object>>) competitorAnalysis
                        .get("marketShareChart");
                if (marketShareChart != null && !marketShareChart.isEmpty()) {
                    chartsData.put("marketShareChart", marketShareChart);
                }

                // 2.2. Competitors Details (аналог старого competitors)
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> competitorsDetails = (List<Map<String, Object>>) competitorAnalysis
                        .get("competitorsDetails");
                if (competitorsDetails != null && !competitorsDetails.isEmpty()) {
                    List<Map<String, Object>> competitorsList = new ArrayList<>();
                    for (Map<String, Object> comp : competitorsDetails) {
                        Map<String, Object> competitorData = new HashMap<>();
                        competitorData.put("name", comp.getOrDefault("name", "Неизвестный конкурент"));

                        // Создаем вложенную структуру digitalMetrics
                        @SuppressWarnings("unchecked")
                        Map<String, Object> digitalMetrics = (Map<String, Object>) comp.get("digitalMetrics");
                        Map<String, Object> metrics = new HashMap<>();
                        metrics.put("reach",
                                digitalMetrics != null ? digitalMetrics.getOrDefault("reach", 10000) : 10000);
                        metrics.put("followers",
                                digitalMetrics != null ? digitalMetrics.getOrDefault("followers", 0) : 0);
                        metrics.put("activityIndex",
                                digitalMetrics != null ? digitalMetrics.getOrDefault("activityIndex", 50) : 50);
                        competitorData.put("digitalMetrics", metrics);

                        competitorData.put("priceStrategy", comp.getOrDefault("priceStrategy", "Не указано"));
                        competitorData.put("channelsHeatmap", comp.getOrDefault("channelsHeatmap", new HashMap<>()));
                        competitorsList.add(competitorData);
                    }
                    chartsData.put("competitors", competitorsList);
                }

                // 2.3. Comparison Table
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> comparisonTable = (List<Map<String, Object>>) competitorAnalysis
                        .get("comparisonTable");
                if (comparisonTable != null && !comparisonTable.isEmpty()) {
                    chartsData.put("comparisonTable", comparisonTable);
                }

                // 2.4. SWOT Competitors (частичный SWOT из competitorAnalysis)
                @SuppressWarnings("unchecked")
                Map<String, Object> swotCompetitors = (Map<String, Object>) competitorAnalysis.get("swotCompetitors");
                if (swotCompetitors != null) {
                    chartsData.put("swotCompetitors", swotCompetitors);
                }
            }

            // 3. Анализ аудитории (новая структура: audienceAnalysis)
            @SuppressWarnings("unchecked")
            Map<String, Object> audienceAnalysis = (Map<String, Object>) analysisJson.get("audienceAnalysis");
            if (audienceAnalysis != null) {
                Map<String, Object> segmentationData = new HashMap<>();

                // 3.1. Age Groups (готовые данные из новой структуры)
                @SuppressWarnings("unchecked")
                Map<String, Object> ageGroupsMap = (Map<String, Object>) audienceAnalysis.get("ageGroups");
                if (ageGroupsMap != null) {
                    // CRITICAL: отдельный ключ для графика возраста (для
                    // [[CHART_AGE_DISTRIBUTION]])
                    // Дальше он будет преобразован в Chart.js формат в transformToChartConfig()
                    if (!ageGroupsMap.isEmpty()) {
                        chartsData.put("audienceAge", ageGroupsMap);
                    }

                    List<Map<String, String>> ageGroupsList = new ArrayList<>();
                    for (Map.Entry<String, Object> entry : ageGroupsMap.entrySet()) {
                        Map<String, String> ageGroup = new HashMap<>();
                        ageGroup.put("label", entry.getKey());
                        ageGroup.put("value", String.valueOf(entry.getValue()));
                        ageGroupsList.add(ageGroup);
                    }
                    segmentationData.put("ageGroups", ageGroupsList);
                }

                // 3.2. Gender Distribution (готовые данные из новой структуры)
                @SuppressWarnings("unchecked")
                Map<String, Object> genderDistributionMap = (Map<String, Object>) audienceAnalysis
                        .get("genderDistribution");
                if (genderDistributionMap != null) {
                    // CRITICAL: отдельный ключ для графика гендера (для
                    // [[CHART_GENDER_DISTRIBUTION]])
                    // Дальше он будет преобразован в Chart.js формат в transformToChartConfig()
                    if (!genderDistributionMap.isEmpty()) {
                        chartsData.put("audienceGender", genderDistributionMap);
                    }

                    List<Map<String, String>> gendersList = new ArrayList<>();
                    for (Map.Entry<String, Object> entry : genderDistributionMap.entrySet()) {
                        Map<String, String> genderData = new HashMap<>();
                        genderData.put("label", entry.getKey());
                        genderData.put("value", String.valueOf(entry.getValue()));
                        gendersList.add(genderData);
                    }
                    segmentationData.put("genders", gendersList);
                }

                // 3.3. Segments (детальные карточки сегментов)
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> segments = (List<Map<String, Object>>) audienceAnalysis.get("segments");
                if (segments != null && !segments.isEmpty()) {
                    List<Map<String, String>> segmentsList = new ArrayList<>();
                    for (Map<String, Object> segment : segments) {
                        Map<String, String> segmentData = new HashMap<>();
                        segmentData.put("label", (String) segment.getOrDefault("name", "Сегмент"));
                        Object percentObj = segment.get("sharePercent");
                        segmentData.put("value", String.valueOf(percentObj instanceof Number ? percentObj : 0));
                        segmentsList.add(segmentData);
                    }
                    segmentationData.put("segments", segmentsList);
                }

                // 3.4. Segments Channel Matrix (heatmap)
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> segmentsChannelMatrix = (List<Map<String, Object>>) audienceAnalysis
                        .get("segmentsChannelMatrix");
                if (segmentsChannelMatrix != null && !segmentsChannelMatrix.isEmpty()) {
                    segmentationData.put("segmentsChannelMatrix", segmentsChannelMatrix);
                }

                // 3.5. Key Takeaways
                @SuppressWarnings("unchecked")
                List<String> keyTakeaways = (List<String>) audienceAnalysis.get("keyTakeaways");
                if (keyTakeaways != null && !keyTakeaways.isEmpty()) {
                    segmentationData.put("keyTakeaways", keyTakeaways);
                }

                if (!segmentationData.isEmpty()) {
                    chartsData.put("audienceSegmentation", segmentationData);
                }
            }

            // 4. Рынок (новая секция)
            @SuppressWarnings("unchecked")
            Map<String, Object> market = (Map<String, Object>) analysisJson.get("market");
            if (market != null) {
                chartsData.put("market", market);
            }

            // 5. Стратегия, воронка, каналы и SWOT (новая структура: strategyAnalysis)
            @SuppressWarnings("unchecked")
            Map<String, Object> strategyAnalysis = (Map<String, Object>) analysisJson.get("strategyAnalysis");
            if (strategyAnalysis != null) {
                // 5.1. Воронка конверсии
                @SuppressWarnings("unchecked")
                Map<String, Object> funnel = (Map<String, Object>) strategyAnalysis.get("funnel");
                if (funnel != null) {
                    @SuppressWarnings("unchecked")
                    List<Map<String, Object>> stages = (List<Map<String, Object>>) funnel.get("stages");
                    if (stages != null && !stages.isEmpty()) {
                        List<Map<String, Object>> funnelList = new ArrayList<>();
                        for (Map<String, Object> stage : stages) {
                            Map<String, Object> stageData = new HashMap<>();
                            stageData.put("stage", stage.getOrDefault("stageName", "Этап"));
                            stageData.put("value", stage.getOrDefault("value", 0));

                            Object conversionObj = stage.get("conversionPercent");
                            if (conversionObj instanceof Number) {
                                stageData.put("conversion", ((Number) conversionObj).intValue());
                            } else {
                                stageData.put("conversion", 0);
                            }
                            funnelList.add(stageData);
                        }
                        chartsData.put("conversionFunnel", funnelList);
                    }
                }

                // 5.2. Потенциал каналов (channelsPotential)
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> channelsPotential = (List<Map<String, Object>>) strategyAnalysis
                        .get("channelsPotential");
                if (channelsPotential != null && !channelsPotential.isEmpty()) {
                    List<Map<String, Object>> channelsList = new ArrayList<>();
                    for (Map<String, Object> channel : channelsPotential) {
                        Map<String, Object> channelData = new HashMap<>();
                        channelData.put("name", channel.getOrDefault("name", "Неизвестный канал"));
                        channelData.put("potential", channel.getOrDefault("potentialIndex", 50));
                        channelData.put("priority", channel.getOrDefault("priority", "Medium"));
                        channelsList.add(channelData);
                    }
                    chartsData.put("channelsPotential", channelsList);
                }

                // 5.3. SWOT-анализ (globalSwot)
                @SuppressWarnings("unchecked")
                Map<String, Object> globalSwot = (Map<String, Object>) strategyAnalysis.get("globalSwot");
                if (globalSwot != null) {
                    chartsData.put("swot", globalSwot);
                }
            }

            // 6. Обратная совместимость: проверяем старую структуру для channels, funnel,
            // swot
            // (на случай, если где-то еще используется старая структура)
            // Примечание: новые данные из strategyAnalysis имеют приоритет - не
            // перезаписываем, если уже есть
            if (!chartsData.containsKey("channelsPotential")) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> channels = (List<Map<String, Object>>) analysisJson.get("channels");
                if (channels != null && !channels.isEmpty()) {
                    List<Map<String, Object>> channelsList = new ArrayList<>();
                    for (Map<String, Object> channel : channels) {
                        Map<String, Object> channelData = new HashMap<>();
                        channelData.put("name", channel.getOrDefault("name", "Неизвестный канал"));
                        channelData.put("potential", channel.getOrDefault("potentialIndex", 50));
                        channelData.put("justification",
                                channel.getOrDefault("justification", "Подходит для данной ниши"));
                        channelsList.add(channelData);
                    }
                    chartsData.put("channelsPotential", channelsList);
                }
            }

            if (!chartsData.containsKey("conversionFunnel")) {
                @SuppressWarnings("unchecked")
                Map<String, Object> funnel = (Map<String, Object>) analysisJson.get("funnel");
                if (funnel != null) {
                    @SuppressWarnings("unchecked")
                    List<Map<String, Object>> stages = (List<Map<String, Object>>) funnel.get("stages");
                    if (stages != null && !stages.isEmpty()) {
                        List<Map<String, Object>> funnelList = new ArrayList<>();
                        for (Map<String, Object> stage : stages) {
                            Map<String, Object> stageData = new HashMap<>();
                            stageData.put("stage", stage.getOrDefault("stageName", "Этап"));
                            stageData.put("value", stage.getOrDefault("value", 0));

                            Object conversionObj = stage.get("conversionPercent");
                            if (conversionObj instanceof Number) {
                                stageData.put("conversion", ((Number) conversionObj).intValue());
                            } else {
                                stageData.put("conversion", 0);
                            }
                            funnelList.add(stageData);
                        }
                        chartsData.put("conversionFunnel", funnelList);
                    }
                }
            }

            if (!chartsData.containsKey("swot")) {
                @SuppressWarnings("unchecked")
                Map<String, Object> swot = (Map<String, Object>) analysisJson.get("swot");
                if (swot != null) {
                    chartsData.put("swot", swot);
                }
            }

        } catch (Exception e) {
            logger.error("parseChartDataFromJson: Error parsing chart data from JSON: {}", e.getMessage(), e);
            logger.error("parseChartDataFromJson: Stack trace: ", e);
        }

        logger.info("parseChartDataFromJson: Chart data parsing completed. Extracted keys: {}", chartsData.keySet());
        return chartsData;
    }

    /**
     * Преобразует сырые данные графиков в формат Chart.js (labels, datasets).
     * 
     * @param rawData сырые данные из parseChartDataFromJson
     * @return Map с данными в формате Chart.js для графиков и исходными данными для
     *         таблиц
     */
    private Map<String, Object> transformToChartConfig(Map<String, Object> rawData) {
        logger.info("transformToChartConfig: Starting transformation to Chart.js format");
        Map<String, Object> formattedCharts = new HashMap<>();

        if (rawData == null || rawData.isEmpty()) {
            logger.warn("transformToChartConfig: rawData is null or empty");
            return formattedCharts;
        }

        try {
            // 1. Seasonality (Map<String, Integer>)
            if (rawData.containsKey("seasonality")) {
                @SuppressWarnings("unchecked")
                Map<String, Integer> seasonality = (Map<String, Integer>) rawData.get("seasonality");
                if (seasonality != null && !seasonality.isEmpty()) {
                    List<String> labels = new ArrayList<>(seasonality.keySet());
                    List<Integer> data = new ArrayList<>(seasonality.values());

                    Map<String, Object> dataset = new HashMap<>();
                    dataset.put("label", "Индекс спроса");
                    dataset.put("data", data);

                    Map<String, Object> chart = new HashMap<>();
                    chart.put("labels", labels);
                    chart.put("datasets", List.of(dataset));
                    formattedCharts.put("seasonality", chart);

                    logger.debug("transformToChartConfig: Transformed seasonality chart with {} labels", labels.size());
                }
            }

            // 2. Competitors (List<Map>)
            if (rawData.containsKey("competitors")) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> competitors = (List<Map<String, Object>>) rawData.get("competitors");
                if (competitors != null && !competitors.isEmpty()) {
                    List<String> labels = new ArrayList<>();
                    List<Integer> data = new ArrayList<>();

                    for (Map<String, Object> competitor : competitors) {
                        labels.add((String) competitor.getOrDefault("name", "Unknown"));

                        // Безопасное извлечение digitalMetrics.reach
                        Object metricsObj = competitor.get("digitalMetrics");
                        if (metricsObj instanceof Map) {
                            @SuppressWarnings("unchecked")
                            Map<String, Object> digitalMetrics = (Map<String, Object>) metricsObj;
                            Object reachObj = digitalMetrics.get("reach");
                            if (reachObj instanceof Number) {
                                data.add(((Number) reachObj).intValue());
                            } else {
                                data.add(0);
                            }
                        } else {
                            data.add(0);
                        }
                    }

                    Map<String, Object> dataset = new HashMap<>();
                    dataset.put("label", "Охват аудитории");
                    dataset.put("data", data);

                    Map<String, Object> chart = new HashMap<>();
                    chart.put("labels", labels);
                    chart.put("datasets", List.of(dataset));
                    formattedCharts.put("competitors", chart);

                    // Сохраняем исходные данные конкурентов для таблиц
                    formattedCharts.put("competitorsRaw", competitors);

                    logger.debug("transformToChartConfig: Transformed competitors chart with {} entries",
                            labels.size());
                }
            }

            // 3. Audience Segmentation (Map с ageGroups и genders)
            if (rawData.containsKey("audienceSegmentation")) {
                @SuppressWarnings("unchecked")
                Map<String, Object> segmentation = (Map<String, Object>) rawData.get("audienceSegmentation");
                if (segmentation != null) {
                    // 3.1. Audience Age Chart
                    if (segmentation.containsKey("ageGroups")) {
                        @SuppressWarnings("unchecked")
                        List<Map<String, String>> ageGroups = (List<Map<String, String>>) segmentation.get("ageGroups");
                        if (ageGroups != null && !ageGroups.isEmpty()) {
                            List<String> labels = new ArrayList<>();
                            List<Number> data = new ArrayList<>();

                            for (Map<String, String> ageGroup : ageGroups) {
                                labels.add(ageGroup.getOrDefault("label", "Unknown"));
                                String valueStr = ageGroup.getOrDefault("value", "0");
                                try {
                                    data.add(Integer.parseInt(valueStr));
                                } catch (NumberFormatException e) {
                                    data.add(0);
                                }
                            }

                            Map<String, Object> dataset = new HashMap<>();
                            dataset.put("label", "Возрастные группы");
                            dataset.put("data", data);

                            Map<String, Object> chart = new HashMap<>();
                            chart.put("labels", labels);
                            chart.put("datasets", List.of(dataset));
                            formattedCharts.put("audienceAge", chart);

                            logger.debug("transformToChartConfig: Transformed audienceAge chart with {} labels",
                                    labels.size());
                        }
                    }

                    // 3.2. Audience Gender Chart
                    if (segmentation.containsKey("genders")) {
                        @SuppressWarnings("unchecked")
                        List<Map<String, String>> genders = (List<Map<String, String>>) segmentation.get("genders");
                        if (genders != null && !genders.isEmpty()) {
                            List<String> labels = new ArrayList<>();
                            List<Number> data = new ArrayList<>();

                            for (Map<String, String> gender : genders) {
                                labels.add(gender.getOrDefault("label", "Unknown"));
                                String valueStr = gender.getOrDefault("value", "0");
                                try {
                                    data.add(Integer.parseInt(valueStr));
                                } catch (NumberFormatException e) {
                                    data.add(0);
                                }
                            }

                            Map<String, Object> dataset = new HashMap<>();
                            dataset.put("label", "Гендерное распределение");
                            dataset.put("data", data);

                            Map<String, Object> chart = new HashMap<>();
                            chart.put("labels", labels);
                            chart.put("datasets", List.of(dataset));
                            formattedCharts.put("audienceGender", chart);

                            logger.debug("transformToChartConfig: Transformed audienceGender chart with {} labels",
                                    labels.size());
                        }
                    }

                    // Сохраняем остальные данные из audienceSegmentation как есть
                    if (segmentation.containsKey("segments")) {
                        formattedCharts.put("segments", segmentation.get("segments"));
                    }
                    if (segmentation.containsKey("segmentsChannelMatrix")) {
                        formattedCharts.put("segmentsChannelMatrix", segmentation.get("segmentsChannelMatrix"));
                    }
                    if (segmentation.containsKey("keyTakeaways")) {
                        formattedCharts.put("keyTakeaways", segmentation.get("keyTakeaways"));
                    }

                    // Сохраняем исходную структуру audienceSegmentation для обратной совместимости
                    formattedCharts.put("audienceSegmentation", segmentation);
                }
            }

            // 3b. Audience Age (CRITICAL): прямой источник из audienceAnalysis.ageGroups
            // Формат rawData.audienceAge: Map<String, Object> ("18-24"->число, ...)
            if (rawData.containsKey("audienceAge") && !formattedCharts.containsKey("audienceAge")) {
                Object ageObj = rawData.get("audienceAge");
                if (ageObj instanceof Map) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> ageMap = (Map<String, Object>) ageObj;
                    if (ageMap != null && !ageMap.isEmpty()) {
                        List<String> labels = new ArrayList<>(ageMap.keySet());
                        List<Number> data = new ArrayList<>();
                        for (String k : labels) {
                            Object v = ageMap.get(k);
                            if (v instanceof Number) {
                                data.add(((Number) v).intValue());
                            } else {
                                try {
                                    data.add(Integer.parseInt(String.valueOf(v)));
                                } catch (Exception e) {
                                    data.add(0);
                                }
                            }
                        }
                        Map<String, Object> dataset = new HashMap<>();
                        dataset.put("label", "Возрастные группы");
                        dataset.put("data", data);
                        Map<String, Object> chart = new HashMap<>();
                        chart.put("labels", labels);
                        chart.put("datasets", List.of(dataset));
                        formattedCharts.put("audienceAge", chart);
                    }
                }
            }

            // 3c. Audience Gender (CRITICAL): прямой источник из
            // audienceAnalysis.genderDistribution
            // Формат rawData.audienceGender: Map<String, Object> ("Женщины"->число, ...)
            if (rawData.containsKey("audienceGender") && !formattedCharts.containsKey("audienceGender")) {
                Object genderObj = rawData.get("audienceGender");
                if (genderObj instanceof Map) {
                    @SuppressWarnings("unchecked")
                    Map<String, Object> genderMap = (Map<String, Object>) genderObj;
                    if (genderMap != null && !genderMap.isEmpty()) {
                        List<String> labels = new ArrayList<>(genderMap.keySet());
                        List<Number> data = new ArrayList<>();
                        for (String k : labels) {
                            Object v = genderMap.get(k);
                            if (v instanceof Number) {
                                data.add(((Number) v).intValue());
                            } else {
                                try {
                                    data.add(Integer.parseInt(String.valueOf(v)));
                                } catch (Exception e) {
                                    data.add(0);
                                }
                            }
                        }
                        Map<String, Object> dataset = new HashMap<>();
                        dataset.put("label", "Гендерное распределение");
                        dataset.put("data", data);
                        Map<String, Object> chart = new HashMap<>();
                        chart.put("labels", labels);
                        chart.put("datasets", List.of(dataset));
                        formattedCharts.put("audienceGender", chart);
                    }
                }
            }

            // 4. Conversion Funnel (List<Map>)
            if (rawData.containsKey("conversionFunnel")) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> funnel = (List<Map<String, Object>>) rawData.get("conversionFunnel");
                if (funnel != null && !funnel.isEmpty()) {
                    List<String> labels = new ArrayList<>();
                    List<Number> data = new ArrayList<>();

                    for (Map<String, Object> stage : funnel) {
                        labels.add((String) stage.getOrDefault("stage", "Unknown"));
                        Object valueObj = stage.get("value");
                        if (valueObj instanceof Number) {
                            data.add(((Number) valueObj).intValue());
                        } else {
                            data.add(0);
                        }
                    }

                    Map<String, Object> dataset = new HashMap<>();
                    dataset.put("label", "Воронка");
                    dataset.put("data", data);

                    Map<String, Object> chart = new HashMap<>();
                    chart.put("labels", labels);
                    chart.put("datasets", List.of(dataset));
                    formattedCharts.put("conversionFunnel", chart);

                    logger.debug("transformToChartConfig: Transformed conversionFunnel chart with {} stages",
                            labels.size());
                }
            }

            // 5. Channels Potential (List<Map>)
            if (rawData.containsKey("channelsPotential")) {
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> channels = (List<Map<String, Object>>) rawData.get("channelsPotential");
                if (channels != null && !channels.isEmpty()) {
                    List<String> labels = new ArrayList<>();
                    List<Number> data = new ArrayList<>();

                    for (Map<String, Object> channel : channels) {
                        labels.add((String) channel.getOrDefault("name", "Unknown"));
                        Object potentialObj = channel.get("potential");
                        if (potentialObj instanceof Number) {
                            data.add(((Number) potentialObj).intValue());
                        } else {
                            data.add(0);
                        }
                    }

                    Map<String, Object> dataset = new HashMap<>();
                    dataset.put("label", "Потенциал");
                    dataset.put("data", data);

                    Map<String, Object> chart = new HashMap<>();
                    chart.put("labels", labels);
                    chart.put("datasets", List.of(dataset));
                    formattedCharts.put("channelsPotential", chart);

                    logger.debug("transformToChartConfig: Transformed channelsPotential chart with {} channels",
                            labels.size());
                }
            }

            // 6. Данные без преобразования (оставить как есть)
            if (rawData.containsKey("swot")) {
                formattedCharts.put("swot", rawData.get("swot"));
            }
            if (rawData.containsKey("comparisonTable")) {
                formattedCharts.put("comparisonTable", rawData.get("comparisonTable"));
            }
            if (rawData.containsKey("marketShareChart")) {
                // CRITICAL: marketShareChart должен быть Chart.js config для вставки в
                // fullAnalysis
                // raw формат: List<{name,value}>. Преобразуем в {labels,datasets}
                @SuppressWarnings("unchecked")
                List<Map<String, Object>> marketShare = (List<Map<String, Object>>) rawData.get("marketShareChart");
                if (marketShare != null && !marketShare.isEmpty()) {
                    List<String> labels = new ArrayList<>();
                    List<Number> data = new ArrayList<>();
                    for (Map<String, Object> item : marketShare) {
                        labels.add(String.valueOf(item.getOrDefault("name", "Unknown")));
                        Object v = item.get("value");
                        if (v instanceof Number) {
                            data.add(((Number) v).intValue());
                        } else {
                            try {
                                data.add(Integer.parseInt(String.valueOf(v)));
                            } catch (Exception e) {
                                data.add(0);
                            }
                        }
                    }
                    Map<String, Object> dataset = new HashMap<>();
                    dataset.put("label", "Доли рынка (%)");
                    dataset.put("data", data);

                    Map<String, Object> chart = new HashMap<>();
                    chart.put("labels", labels);
                    chart.put("datasets", List.of(dataset));
                    formattedCharts.put("marketShareChart", chart);
                }
            }
            if (rawData.containsKey("swotCompetitors")) {
                formattedCharts.put("swotCompetitors", rawData.get("swotCompetitors"));
            }
            if (rawData.containsKey("market")) {
                formattedCharts.put("market", rawData.get("market"));
            }

        } catch (Exception e) {
            logger.error("transformToChartConfig: Error transforming chart data: {}", e.getMessage(), e);
            logger.error("transformToChartConfig: Stack trace: ", e);
        }

        logger.info("transformToChartConfig: Transformation completed. Formatted charts keys: {}",
                formattedCharts.keySet());
        return formattedCharts;
    }

    private Map<String, Object> generateMarketingReport(MarketingAnalysisRequest request) {
        logger.info("generateMarketingReport: Starting report generation for product: {}", request.getProduct());
        Map<String, Object> report = new HashMap<>();

        // Web search is mandatory before generating any report parts.
        Map<String, Object> researchPack = buildResearchPack(request);
        report.put("researchPack", researchPack);

        // Get all analysis types and detail level
        List<AnalysisType> analysisTypes = request.getAnalysisTypeEnums();
        if (analysisTypes == null || analysisTypes.isEmpty()) {
            logger.warn("generateMarketingReport: No analysis types provided, using default: РЫНОК");
            analysisTypes = List.of(AnalysisType.РЫНОК); // Default
        }
        DetailLevel detailLevel = request.getDetailLevelEnum();
        String detailLevelName = detailLevel != null ? detailLevel.getDisplayName() : "Стандартно";
        logger.debug("generateMarketingReport: Analysis types: {}, Detail level: {}", analysisTypes, detailLevelName);

        // Build list of analysis type names for display
        List<String> analysisTypeNames = analysisTypes.stream()
                .map(AnalysisType::getDisplayName)
                .toList();

        // Шаг 1: Генерация JSON-анализа (Промпт №1)
        // Контекст (product, businessNiche, goal, regions) добавляется в analysisJson
        // после генерации
        logger.info("generateMarketingReport: Starting JSON analysis generation");
        Map<String, Object> analysisJson = generateJsonAnalysis(request);

        if (analysisJson == null || analysisJson.isEmpty()) {
            logger.error(
                    "generateMarketingReport: Failed to generate JSON analysis - likely due to OpenAI API rate limits");
            // При ошибке возвращаем базовую структуру с сообщением об ошибке
            report.put("analysisTypes", analysisTypeNames);
            report.put("detailLevel", detailLevelName);
            report.put("fullAnalysis", "Не удалось сгенерировать анализ из-за превышения лимита запросов к OpenAI API. "
                    +
                    "Система автоматически повторит попытку с задержкой. Пожалуйста, попробуйте позже или уменьшите частоту запросов.");
            report.put("summary", "Маркетинговый анализ для " + request.getProduct() +
                    " (временно недоступен из-за лимита API)");
            report.put("chartsData", new HashMap<>());

            logger.warn("generateMarketingReport: Returning report with rate limit error message");
        } else {
            logger.info("generateMarketingReport: JSON analysis generated successfully. Keys: {}",
                    analysisJson.keySet());

            // Добавляем контекст продукта в analysisJson для использования в AI-генерации
            // текста
            analysisJson.put("product", request.getProduct());
            analysisJson.put("businessNiche", request.getBusinessNiche());
            analysisJson.put("goal", request.getGoal());
            analysisJson.put("regions", request.getRegion());
            analysisJson.put("detailLevel", detailLevelName);
            analysisJson.put("researchPack", researchPack);

            // Сохраняем JSON в отчёт
            report.put("analysisJson", analysisJson);
            // Make key market fields available at top-level for markdown/table rendering
            if (analysisJson.get("market") != null) {
                report.put("market", analysisJson.get("market"));
            }

            // Шаг 2: Генерация профессионального текстового отчёта (11 последовательных
            // вызовов AI)
            logger.info("generateMarketingReport: Starting modular text report generation (11 sections)");

            // Последовательное выполнение всех разделов
            StringBuilder fullReportBuilder = new StringBuilder();
            try {
                String s1 = genSectionSummary(prepareSectionContext(analysisJson, "summary"));
                if (s1 != null && !s1.trim().isEmpty() && isValidAiResponse(s1)) {
                    // Очищаем от ошибок перед добавлением
                    String cleaned = removeErrorMessages(s1);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned).append("\n\n");
                    } else {
                        logger.warn("generateMarketingReport: Section I (Summary) contained only error messages");
                        fullReportBuilder.append("I. Краткое резюме\n\nНе удалось сгенерировать раздел.\n\n");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section I (Summary) failed to generate");
                    fullReportBuilder.append("I. Краткое резюме\n\nНе удалось сгенерировать раздел.\n\n");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section I: {}", e.getMessage(), e);
                fullReportBuilder.append("I. Краткое резюме\n\nОшибка при генерации раздела.\n\n");
            }

            try {
                String s2 = genSectionProduct(prepareSectionContext(analysisJson, "product"));
                if (s2 != null && !s2.trim().isEmpty() && isValidAiResponse(s2)) {
                    String cleaned = removeErrorMessages(s2);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned).append("\n\n");
                    } else {
                        logger.warn("generateMarketingReport: Section II (Product) contained only error messages");
                        fullReportBuilder.append("II. Анализ продукта\n\nНе удалось сгенерировать раздел.\n\n");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section II (Product) failed to generate");
                    fullReportBuilder.append("II. Анализ продукта\n\nНе удалось сгенерировать раздел.\n\n");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section II: {}", e.getMessage(), e);
                fullReportBuilder.append("II. Анализ продукта\n\nОшибка при генерации раздела.\n\n");
            }

            try {
                String s3 = genSectionMarket(prepareSectionContext(analysisJson, "market"));
                if (s3 != null && !s3.trim().isEmpty() && isValidAiResponse(s3)) {
                    String cleaned = removeErrorMessages(s3);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned).append("\n\n");
                    } else {
                        logger.warn("generateMarketingReport: Section III (Market) contained only error messages");
                        fullReportBuilder
                                .append("III. Анализ рынка и сезонности\n\nНе удалось сгенерировать раздел.\n\n");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section III (Market) failed to generate");
                    fullReportBuilder.append("III. Анализ рынка и сезонности\n\nНе удалось сгенерировать раздел.\n\n");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section III: {}", e.getMessage(), e);
                fullReportBuilder.append("III. Анализ рынка и сезонности\n\nОшибка при генерации раздела.\n\n");
            }

            try {
                String s4 = genSectionAudience(prepareSectionContext(analysisJson, "audience"));
                if (s4 != null && !s4.trim().isEmpty() && isValidAiResponse(s4)) {
                    String cleaned = removeErrorMessages(s4);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned).append("\n\n");
                    } else {
                        logger.warn("generateMarketingReport: Section IV (Audience) contained only error messages");
                        fullReportBuilder
                                .append("IV. Анализ целевой аудитории\n\nНе удалось сгенерировать раздел.\n\n");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section IV (Audience) failed to generate");
                    fullReportBuilder.append("IV. Анализ целевой аудитории\n\nНе удалось сгенерировать раздел.\n\n");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section IV: {}", e.getMessage(), e);
                fullReportBuilder.append("IV. Анализ целевой аудитории\n\nОшибка при генерации раздела.\n\n");
            }

            try {
                String s5 = genSectionCompetitors(prepareSectionContext(analysisJson, "competitors"));
                if (s5 != null && !s5.trim().isEmpty() && isValidAiResponse(s5)) {
                    String cleaned = removeErrorMessages(s5);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned).append("\n\n");
                    } else {
                        logger.warn("generateMarketingReport: Section V (Competitors) contained only error messages");
                        fullReportBuilder.append("V. Анализ конкурентов\n\nНе удалось сгенерировать раздел.\n\n");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section V (Competitors) failed to generate");
                    fullReportBuilder.append("V. Анализ конкурентов\n\nНе удалось сгенерировать раздел.\n\n");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section V: {}", e.getMessage(), e);
                fullReportBuilder.append("V. Анализ конкурентов\n\nОшибка при генерации раздела.\n\n");
            }

            try {
                String s6 = genSectionSWOT(prepareSectionContext(analysisJson, "swot"));
                if (s6 != null && !s6.trim().isEmpty() && isValidAiResponse(s6)) {
                    String cleaned = removeErrorMessages(s6);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned).append("\n\n");
                    } else {
                        logger.warn("generateMarketingReport: Section VI (SWOT) contained only error messages");
                        fullReportBuilder.append("VI. SWOT-анализ\n\nНе удалось сгенерировать раздел.\n\n");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section VI (SWOT) failed to generate");
                    fullReportBuilder.append("VI. SWOT-анализ\n\nНе удалось сгенерировать раздел.\n\n");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section VI: {}", e.getMessage(), e);
                fullReportBuilder.append("VI. SWOT-анализ\n\nОшибка при генерации раздела.\n\n");
            }

            try {
                String s7 = genSectionChannels(prepareSectionContext(analysisJson, "channels"));
                if (s7 != null && !s7.trim().isEmpty() && isValidAiResponse(s7)) {
                    String cleaned = removeErrorMessages(s7);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned).append("\n\n");
                    } else {
                        logger.warn("generateMarketingReport: Section VII (Channels) contained only error messages");
                        fullReportBuilder
                                .append("VII. Каналы продвижения и их потенциал\n\nНе удалось сгенерировать раздел.\n\n");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section VII (Channels) failed to generate");
                    fullReportBuilder
                            .append("VII. Каналы продвижения и их потенциал\n\nНе удалось сгенерировать раздел.\n\n");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section VII: {}", e.getMessage(), e);
                fullReportBuilder.append("VII. Каналы продвижения и их потенциал\n\nОшибка при генерации раздела.\n\n");
            }

            try {
                String s8 = genSectionFunnel(prepareSectionContext(analysisJson, "funnel"));
                if (s8 != null && !s8.trim().isEmpty() && isValidAiResponse(s8)) {
                    String cleaned = removeErrorMessages(s8);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned).append("\n\n");
                    } else {
                        logger.warn("generateMarketingReport: Section VIII (Funnel) contained only error messages");
                        fullReportBuilder.append("VIII. Воронка спроса\n\nНе удалось сгенерировать раздел.\n\n");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section VIII (Funnel) failed to generate");
                    fullReportBuilder.append("VIII. Воронка спроса\n\nНе удалось сгенерировать раздел.\n\n");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section VIII: {}", e.getMessage(), e);
                fullReportBuilder.append("VIII. Воронка спроса\n\nОшибка при генерации раздела.\n\n");
            }

            try {
                String s9 = genSectionPositioning(prepareSectionContext(analysisJson, "positioning"));
                if (s9 != null && !s9.trim().isEmpty() && isValidAiResponse(s9)) {
                    String cleaned = removeErrorMessages(s9);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned).append("\n\n");
                    } else {
                        logger.warn("generateMarketingReport: Section IX (Positioning) contained only error messages");
                        fullReportBuilder.append(
                                "IX. Рекомендации по позиционированию и УТП\n\nНе удалось сгенерировать раздел.\n\n");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section IX (Positioning) failed to generate");
                    fullReportBuilder.append(
                            "IX. Рекомендации по позиционированию и УТП\n\nНе удалось сгенерировать раздел.\n\n");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section IX: {}", e.getMessage(), e);
                fullReportBuilder
                        .append("IX. Рекомендации по позиционированию и УТП\n\nОшибка при генерации раздела.\n\n");
            }

            try {
                String s10 = genSectionContent(prepareSectionContext(analysisJson, "content"));
                if (s10 != null && !s10.trim().isEmpty() && isValidAiResponse(s10)) {
                    String cleaned = removeErrorMessages(s10);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned).append("\n\n");
                    } else {
                        logger.warn("generateMarketingReport: Section X (Content) contained only error messages");
                        fullReportBuilder.append("X. Рекомендации по контенту\n\nНе удалось сгенерировать раздел.\n\n");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section X (Content) failed to generate");
                    fullReportBuilder.append("X. Рекомендации по контенту\n\nНе удалось сгенерировать раздел.\n\n");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section X: {}", e.getMessage(), e);
                fullReportBuilder.append("X. Рекомендации по контенту\n\nОшибка при генерации раздела.\n\n");
            }

            try {
                String s11 = genSectionStrategy(prepareSectionContext(analysisJson, "strategy"));
                if (s11 != null && !s11.trim().isEmpty() && isValidAiResponse(s11)) {
                    String cleaned = removeErrorMessages(s11);
                    if (cleaned != null && !cleaned.trim().isEmpty()) {
                        fullReportBuilder.append(cleaned);
                    } else {
                        logger.warn("generateMarketingReport: Section XI (Strategy) contained only error messages");
                        fullReportBuilder.append(
                                "XI. Итоговая стратегия: что делать в первую очередь\n\nНе удалось сгенерировать раздел.");
                    }
                } else {
                    logger.warn("generateMarketingReport: Section XI (Strategy) failed to generate");
                    fullReportBuilder.append(
                            "XI. Итоговая стратегия: что делать в первую очередь\n\nНе удалось сгенерировать раздел.");
                }
            } catch (Exception e) {
                logger.error("generateMarketingReport: Error getting section XI: {}", e.getMessage(), e);
                fullReportBuilder
                        .append("XI. Итоговая стратегия: что делать в первую очередь\n\nОшибка при генерации раздела.");
            }

            String textReport = fullReportBuilder.toString();
            logger.info("generateMarketingReport: Modular text report generated. Length: {} chars",
                    textReport != null ? textReport.length() : 0);
            // Hard sanitize forbidden "no data" wording before placeholder replacement
            textReport = hardSanitizeReportText(textReport, false);

            // Извлекаем данные для графиков из JSON
            logger.info("generateMarketingReport: Starting chart data parsing from JSON");
            Map<String, Object> chartsData = parseChartDataFromJson(analysisJson);
            logger.info("generateMarketingReport: Chart data parsed. Keys: {}",
                    chartsData != null ? chartsData.keySet() : "null");

            // Преобразуем данные в формат Chart.js
            Map<String, Object> readyToRenderCharts = transformToChartConfig(chartsData);
            logger.info("generateMarketingReport: Chart data transformed to Chart.js format. Keys: {}",
                    readyToRenderCharts != null ? readyToRenderCharts.keySet() : "null");
            report.put("chartsData", readyToRenderCharts);

            // Заменяем плейсхолдеры графиков на JSON-блоки
            String processedTextReport = textReport;
            if (textReport != null && readyToRenderCharts != null && !readyToRenderCharts.isEmpty()) {
                logger.info("generateMarketingReport: Replacing chart placeholders in text report");
                processedTextReport = replaceChartPlaceholders(textReport, readyToRenderCharts);
                logger.info("generateMarketingReport: Chart placeholders replaced. Length: {} chars",
                        processedTextReport != null ? processedTextReport.length() : 0);
            } else if (textReport != null) {
                // Если данных графиков нет, просто удаляем плейсхолдеры
                logger.warn("generateMarketingReport: No chart data available, removing placeholders");
                processedTextReport = textReport
                        .replace("[[CHART_SEASONALITY]]", "")
                        .replace("[[CHART_AUDIENCE]]", "")
                        .replace("[[CHART_COMPETITORS]]", "")
                        .replace("[[CHART_MARKET_SHARE]]", "")
                        .replace("[[TABLE_COMPARISON]]", "")
                        .replace("[[CHART_AGE_DISTRIBUTION]]", "")
                        .replace("[[CHART_GENDER_DISTRIBUTION]]", "")
                        .replace("[[CHART_CHANNELS]]", "")
                        .replace("[[CHART_CHANNEL_MATRIX]]", "")
                        .replace("[[CHART_FUNNEL]]", "")
                        .replace("[[CHART_SWOT]]", "");
                processedTextReport = processedTextReport.replaceAll(
                        "(?i)\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_]+\\s*\\]\\]",
                        "");
            }

            // Сохраняем данные анализа
            report.put("analysisTypes", analysisTypeNames);
            report.put("detailLevel", detailLevelName);

            // Очищаем текст: ошибки/отказы + "пустые" фразы про графики без реального блока
            String cleanedTextReport = processedTextReport != null ? removeErrorMessages(processedTextReport) : null;
            cleanedTextReport = cleanedTextReport != null ? removeDanglingChartNarration(cleanedTextReport) : null;
            // Final hard sanitize: remove forbidden "no data" wording and any leftover
            // placeholders
            cleanedTextReport = cleanedTextReport != null ? hardSanitizeReportText(cleanedTextReport, true) : null;
            if (cleanedTextReport != null && cleanedTextReport.trim().isEmpty()) {
                cleanedTextReport = "Анализ не удалось сгенерировать.";
            }
            report.put("fullAnalysis",
                    cleanedTextReport != null ? cleanedTextReport : "Анализ не удалось сгенерировать.");

            // Summary содержит только короткую вводную фразу
            String summary = "Маркетинговый анализ для " + request.getProduct();
            if (request.getBusinessNiche() != null && !request.getBusinessNiche().trim().isEmpty()) {
                summary += " (" + request.getBusinessNiche() + ")";
            }
            report.put("summary", summary);
        }

        logger.info("generateMarketingReport: Report generation completed. Report keys: {}", report.keySet());

        return report;
    }

    private String buildMarkdownFromReport(Map<String, Object> reportData, MarketingAnalysisRequest request) {
        logger.info("buildMarkdownFromReport: Starting markdown generation for product: {}", request.getProduct());
        logger.debug("buildMarkdownFromReport: ReportData keys: {}", reportData != null ? reportData.keySet() : "null");
        StringBuilder markdown = new StringBuilder();

        markdown.append("# Маркетинговый анализ\n\n");

        // Prepare full analysis with stable anchors (I-XI) and consistent heading
        // levels
        String fullAnalysisRaw = (String) reportData.get("fullAnalysis");
        String fullAnalysisWithAnchors = addAnchorsAndHeadingLevelsForSections(fullAnalysisRaw);

        // Interactive TOC (I-XI) with explicit anchors
        markdown.append("## Оглавление\n\n");
        Map<String, String> sectionTitles = extractSectionTitles(fullAnalysisWithAnchors);
        for (String roman : SECTION_ROMANS) {
            String id = ROMAN_TO_ANCHOR_ID.get(roman);
            String label = sectionTitles.getOrDefault(roman, "Раздел " + roman);
            if ("V".equals(roman)
                    && (label == null || label.isBlank()
                            || "Раздел V".equals(label)
                            || "V. Раздел V".equals(label)
                            || label.toLowerCase(Locale.ROOT).contains("раздел v"))) {
                label = "V. Анализ конкурентов";
            }
            markdown.append("- [").append(label).append("](#").append(id).append(")\n");
        }
        markdown.append("\n");

        markdown.append("## Информация о бизнесе\n\n");
        markdown.append("- **Ниша бизнеса:** ").append(request.getBusinessNiche()).append("\n");
        markdown.append("- **Продукт/услуга:** ").append(request.getProduct()).append("\n");

        // Format regions
        List<String> regions = request.getRegion();
        if (regions != null && !regions.isEmpty()) {
            markdown.append("- **Регион:** ").append(String.join(", ", regions)).append("\n");
        }

        markdown.append("- **Цель на 6-12 месяцев:** ").append(request.getGoal()).append("\n");

        String detailLevelName = (String) reportData.get("detailLevel");
        if (detailLevelName != null) {
            markdown.append("- **Уровень детализации:** ").append(detailLevelName).append("\n");
        }

        if (request.getStrongSide() != null && !request.getStrongSide().trim().isEmpty()) {
            markdown.append("- **Сильная сторона:** ").append(request.getStrongSide()).append("\n");
        }
        if (request.getWeakSide() != null && !request.getWeakSide().trim().isEmpty()) {
            markdown.append("- **Слабая сторона:** ").append(request.getWeakSide()).append("\n");
        }

        @SuppressWarnings("unchecked")
        List<String> analysisTypeNames = (List<String>) reportData.get("analysisTypes");
        if (analysisTypeNames != null && !analysisTypeNames.isEmpty()) {
            markdown.append("- **Типы анализа:** ").append(String.join(", ", analysisTypeNames)).append("\n");
        }
        markdown.append("\n");

        // Key metrics block before section I
        String regionStr = "-";
        if (regions != null && !regions.isEmpty()) {
            regionStr = String.join(", ", regions);
        }
        String productStr = request.getProduct() != null ? request.getProduct() : "-";
        String goalStr = request.getGoal() != null ? request.getGoal() : "-";

        markdown.append("## Ключевые метрики проекта\n\n");
        markdown.append("| Метрика | Значение |\n");
        markdown.append("|---|---|\n");
        markdown.append("| Продукт | ").append(productStr).append(" |\n");
        markdown.append("| Регион | ").append(regionStr).append(" |\n");
        markdown.append("| Цель | ").append(goalStr).append(" |\n");
        String marketSizeStr = "-";
        String growthRateStr = "-";
        try {
            @SuppressWarnings("unchecked")
            Map<String, Object> market = reportData != null && reportData.get("market") instanceof Map
                    ? (Map<String, Object>) reportData.get("market")
                    : null;
            if (market != null) {
                Object ms = market.get("marketSize");
                Object gr = market.get("growthRate");
                if (ms != null && !String.valueOf(ms).trim().isEmpty()) {
                    marketSizeStr = String.valueOf(ms).trim();
                }
                if (gr != null && !String.valueOf(gr).trim().isEmpty()) {
                    growthRateStr = String.valueOf(gr).trim();
                }
            }
        } catch (Exception ignore) {
            // Keep '-' if parsing fails
        }
        markdown.append("| Объем рынка | ").append(marketSizeStr).append(" |\n");
        markdown.append("| Темп роста | ").append(growthRateStr).append(" |\n\n");

        // Display full analysis - это основной блок с полным отчетом
        // Все секции (I-XI) уже включены в fullAnalysis благодаря модульной генерации
        // Плейсхолдеры графиков уже заменены в generateMarketingReport
        if (fullAnalysisWithAnchors != null && !fullAnalysisWithAnchors.trim().isEmpty()) {
            // Добавляем полный отчет без дополнительного заголовка,
            // так как fullAnalysis уже начинается с "I. Краткое резюме"
            markdown.append(fullAnalysisWithAnchors).append("\n\n");
        } else {
            // Если fullAnalysis пустой, выводим сообщение об ошибке
            markdown.append("Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже.\n\n");
        }

        return markdown.toString();
    }

    private boolean isEmptyChartData(Object data) {
        if (data == null) {
            return true;
        }
        if (data instanceof Map<?, ?>) {
            return ((Map<?, ?>) data).isEmpty();
        }
        if (data instanceof Collection<?>) {
            return ((Collection<?>) data).isEmpty();
        }
        return false;
    }

    /**
     * Заменяет плейсхолдеры в тексте отчета на JSON-блоки с данными графиков
     *
     * @param fullAnalysis текст отчета с плейсхолдерами
     * @param chartsData   данные для графиков
     * @return текст отчета с замененными плейсхолдерами на JSON-блоки
     */
    private String replaceChartPlaceholders(String fullAnalysis, Map<String, Object> chartsData) {
        logger.info("replaceChartPlaceholders: Starting placeholder replacement");
        logger.debug("replaceChartPlaceholders: FullAnalysis length: {}, ChartsData keys: {}",
                fullAnalysis != null ? fullAnalysis.length() : 0,
                chartsData != null ? chartsData.keySet() : "null");

        if (fullAnalysis == null) {
            logger.warn("replaceChartPlaceholders: fullAnalysis is null, returning empty string");
            return "";
        }

        if (fullAnalysis.trim().isEmpty()) {
            logger.warn("replaceChartPlaceholders: fullAnalysis is empty");
            return fullAnalysis;
        }

        if (chartsData == null || chartsData.isEmpty()) {
            logger.warn("replaceChartPlaceholders: chartsData is null or empty, removing all placeholders");
            String cleaned = fullAnalysis
                    .replace("[[CHART_SEASONALITY]]", "")
                    .replace("[[CHART_AUDIENCE]]", "")
                    .replace("[[CHART_COMPETITORS]]", "")
                    .replace("[[CHART_MARKET_SHARE]]", "")
                    .replace("[[TABLE_COMPARISON]]", "")
                    .replace("[[CHART_AGE_DISTRIBUTION]]", "")
                    .replace("[[CHART_GENDER_DISTRIBUTION]]", "")
                    .replace("[[CHART_CHANNELS]]", "")
                    .replace("[[CHART_CHANNEL_MATRIX]]", "")
                    .replace("[[CHART_FUNNEL]]", "")
                    .replace("[[CHART_SWOT]]", "");
            // Safety net: remove any unknown/variant placeholders too.
            // IMPORTANT: model sometimes outputs generic placeholders like [[CHART_...]].
            // We tolerate dots/hyphens/underscores to ensure they get removed.
            cleaned = cleaned.replaceAll("(?i)\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_.-]+\\s*\\]\\]", "");
            cleaned = cleaned.replaceAll("(\\r?\\n){3,}", "\n\n");
            return cleaned;
        }

        String result = fullAnalysis;
        // Keep normalized placeholders to be resilient to model variations
        Set<String> processedPlaceholders = new HashSet<>();

        Map<String, ChartMapping> placeholderMap = new HashMap<>();
        placeholderMap.put("[[CHART_SEASONALITY]]", new ChartMapping("seasonality", "seasonality"));
        placeholderMap.put("[[CHART_AUDIENCE]]", new ChartMapping("audienceSegmentation", "audienceSegmentation"));
        placeholderMap.put("[[CHART_COMPETITORS]]", new ChartMapping("competitors", "competitors"));
        // CRITICAL mapping (front-end expects exactly these keys in ```json:<key>
        // blocks):
        // [[CHART_MARKET_SHARE]] -> marketShareChart
        // [[TABLE_COMPARISON]] -> comparisonTable
        // [[CHART_AGE_DISTRIBUTION]] -> audienceAge
        // [[CHART_GENDER_DISTRIBUTION]] -> audienceGender
        placeholderMap.put("[[CHART_MARKET_SHARE]]", new ChartMapping("marketShareChart", "marketShareChart"));
        placeholderMap.put("[[TABLE_COMPARISON]]", new ChartMapping("comparisonTable", "comparisonTable"));
        placeholderMap.put("[[CHART_AGE_DISTRIBUTION]]", new ChartMapping("audienceAge", "audienceAge"));
        placeholderMap.put("[[CHART_GENDER_DISTRIBUTION]]", new ChartMapping("audienceGender", "audienceGender"));
        placeholderMap.put("[[CHART_CHANNELS]]", new ChartMapping("channelsPotential", "channelsPotential"));
        placeholderMap.put("[[CHART_CHANNEL_MATRIX]]",
                new ChartMapping("segmentsChannelMatrix", "segmentsChannelMatrix"));
        placeholderMap.put("[[CHART_FUNNEL]]", new ChartMapping("conversionFunnel", "funnel"));
        placeholderMap.put("[[CHART_SWOT]]", new ChartMapping("swot", "swot"));

        // Build normalized map to handle placeholder variants like whitespace/case
        // differences produced by the model
        Map<String, ChartMapping> normalizedPlaceholderMap = new HashMap<>();
        for (Map.Entry<String, ChartMapping> e : placeholderMap.entrySet()) {
            normalizedPlaceholderMap.put(normalizePlaceholderToken(e.getKey()), e.getValue());
        }

        // Find all placeholders actually present in the text (incl. variants with
        // spaces). Use a fast contains() check first, but fall back to regex to
        // tolerate extra spaces like: [[ CHART_SEASONALITY ]].
        java.util.regex.Pattern presentPattern = java.util.regex.Pattern
                .compile("\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_.-]+\\s*\\]\\]");
        Set<String> presentPlaceholders = new LinkedHashSet<>();
        if (containsChartOrTablePlaceholder(result)) {
            java.util.regex.Matcher presentMatcher = presentPattern.matcher(result);
            while (presentMatcher.find()) {
                presentPlaceholders.add(presentMatcher.group());
            }
        }

        for (String placeholderVariant : presentPlaceholders) {
            String normalized = normalizePlaceholderToken(placeholderVariant);
            ChartMapping mapping = normalizedPlaceholderMap.get(normalized);

            if (mapping == null) {
                logger.warn("replaceChartPlaceholders: Unknown placeholder variant found: {}. Removing.",
                        placeholderVariant);
                result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant);
                continue;
            }

            if (!chartsData.containsKey(mapping.dataKey)) {
                logger.warn("replaceChartPlaceholders: chartsData missing key '{}' for placeholder {}",
                        mapping.dataKey, placeholderVariant);
            }
            Object chartData = chartsData.get(mapping.dataKey);
            if (!isEmptyChartData(chartData)) {
                try {
                    String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(chartData);
                    String jsonBlock = buildChartJsonBlock(mapping.chartKey, jsonStr);
                    result = replacePlaceholderVariantWithJsonBlock(result, placeholderVariant, jsonBlock);
                    processedPlaceholders.add(normalized);
                    logger.info("Successfully replaced placeholder: {} (normalized {}) with dataKey: {}",
                            placeholderVariant, normalized, mapping.dataKey);
                } catch (Exception e) {
                    logger.error("Error serializing data for placeholder {} (key: {}): {}",
                            placeholderVariant, mapping.dataKey, e.getMessage(), e);
                    // If serialization fails, do not leave dangling tags in report
                    result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant);
                }
            } else {
                logger.warn("MISSING DATA for placeholder: {} (key: {}). Removing placeholder.", placeholderVariant,
                        mapping.dataKey);
                result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant);
            }
        }

        logger.debug("replaceChartPlaceholders: Adding missing charts to sections");
        result = addMissingChartsToSections(result, chartsData, placeholderMap, processedPlaceholders);

        // Final pass: ensure no placeholders remain. If data exists for a known
        // placeholder but it
        // wasn't replaced due to a model variant, this pass will still replace it.
        java.util.regex.Matcher remaining = presentPattern.matcher(result);
        Set<String> remainingPlaceholders = new LinkedHashSet<>();
        while (remaining.find()) {
            remainingPlaceholders.add(remaining.group());
        }
        for (String placeholderVariant : remainingPlaceholders) {
            String normalized = normalizePlaceholderToken(placeholderVariant);
            ChartMapping mapping = normalizedPlaceholderMap.get(normalized);
            if (mapping != null && !isEmptyChartData(chartsData.get(mapping.dataKey))) {
                try {
                    String jsonStr = objectMapper.writerWithDefaultPrettyPrinter()
                            .writeValueAsString(chartsData.get(mapping.dataKey));
                    String jsonBlock = buildChartJsonBlock(mapping.chartKey, jsonStr);
                    result = replacePlaceholderVariantWithJsonBlock(result, placeholderVariant, jsonBlock);
                    processedPlaceholders.add(normalized);
                    continue;
                } catch (Exception e) {
                    logger.warn("replaceChartPlaceholders: Failed final serialization for {}: {}", placeholderVariant,
                            e.getMessage());
                }
            }
            if (mapping != null && !chartsData.containsKey(mapping.dataKey)) {
                logger.warn("replaceChartPlaceholders: chartsData missing key '{}' for remaining placeholder {}",
                        mapping.dataKey, placeholderVariant);
            }
            logger.warn("replaceChartPlaceholders: Removing leftover placeholder: {}", placeholderVariant);
            result = removePlaceholderWithSurroundingNewlines(result, placeholderVariant);
        }

        // Final safety net: remove any leftover placeholders (known or unknown),
        // including generic tokens like [[CHART_...]].
        result = result.replaceAll("(?i)\\[\\[\\s*(CHART|TABLE)_[A-Za-z0-9_.-]+\\s*\\]\\]", "");
        result = result.replaceAll("(\\r?\\n){3,}", "\n\n");

        logger.info(
                "replaceChartPlaceholders: Placeholder replacement completed. Processed placeholders: {}, Result length: {} chars",
                processedPlaceholders.size(), result.length());
        return result;
    }

    private static class ChartMapping {
        final String dataKey;
        final String chartKey;

        ChartMapping(String dataKey, String chartKey) {
            this.dataKey = dataKey;
            this.chartKey = chartKey;
        }
    }

    private String addMissingChartsToSections(String text, Map<String, Object> chartsData,
            Map<String, ChartMapping> placeholderMap, Set<String> processedPlaceholders) {
        String result = text;
        if (result == null || result.isBlank()) {
            return result;
        }

        // Section -> required placeholders by Roman numeral.
        // Must work for headings like:
        // - "III. Анализ рынка..."
        // - "## III. Анализ рынка..."
        // - "## V. Анализ конкурентов"
        Map<String, List<String>> romanToPlaceholders = new HashMap<>();
        romanToPlaceholders.put("III", List.of("[[CHART_SEASONALITY]]"));
        romanToPlaceholders.put("IV", List.of("[[CHART_AGE_DISTRIBUTION]]", "[[CHART_GENDER_DISTRIBUTION]]"));
        romanToPlaceholders.put("V", List.of("[[CHART_MARKET_SHARE]]", "[[TABLE_COMPARISON]]"));
        romanToPlaceholders.put("VI", List.of("[[CHART_SWOT]]"));
        romanToPlaceholders.put("VII", List.of("[[CHART_CHANNELS]]", "[[CHART_CHANNEL_MATRIX]]"));
        romanToPlaceholders.put("VIII", List.of("[[CHART_FUNNEL]]"));

        // DEDUP RULE:
        // If the model placed at least one placeholder in the text and we successfully
        // replaced it (processedPlaceholders is non-empty), we must NOT append any
        // missing placeholders. Appending at section ends can break layout and can
        // cause "charts flying into TOC" issues.
        if (processedPlaceholders != null && !processedPlaceholders.isEmpty()) {
            return result;
        }

        // Find all section headings by Roman numerals. Allow optional markdown heading
        // prefix.
        // Example matches:
        // "## III. Анализ рынка и сезонности"
        // "V. Анализ конкурентов"
        // IMPORTANT:
        // - Only match real Markdown headings in the body (##, ###, ...). Do NOT match
        // TOC list items like "- [III. ...](#section-iii)".
        java.util.regex.Pattern sectionHeader = java.util.regex.Pattern.compile(
                "(?im)^(?!\\s*-\\s*\\[)\\s*#{2,6}\\s*([IVX]{1,6})\\s*\\.\\s*.*$");
        java.util.regex.Matcher m = sectionHeader.matcher(result);

        class SectionRange {
            final int start;
            final String roman;

            SectionRange(int start, String roman) {
                this.start = start;
                this.roman = roman;
            }
        }

        List<SectionRange> headers = new ArrayList<>();
        while (m.find()) {
            String fullLine = m.group(0);
            if (fullLine != null && fullLine.trim().startsWith("- [")) {
                // Safety net: ignore TOC list items even if regex is modified later
                continue;
            }
            String roman = m.group(1) != null ? m.group(1).toUpperCase() : null;
            if (roman == null || roman.isBlank()) {
                continue;
            }
            if (!romanToPlaceholders.containsKey(roman)) {
                continue;
            }
            headers.add(new SectionRange(m.start(), roman));
        }

        if (headers.isEmpty()) {
            logger.debug("addMissingChartsToSections: No target sections found by roman headers");
            return result;
        }

        // Insert from bottom to top so indices stay valid.
        for (int i = headers.size() - 1; i >= 0; i--) {
            SectionRange h = headers.get(i);
            int sectionStart = h.start;
            int sectionEnd = (i < headers.size() - 1) ? headers.get(i + 1).start : result.length();
            if (sectionStart < 0 || sectionEnd <= sectionStart || sectionEnd > result.length()) {
                continue;
            }

            String sectionText = result.substring(sectionStart, sectionEnd);
            List<String> placeholders = romanToPlaceholders.get(h.roman);
            if (placeholders == null || placeholders.isEmpty()) {
                continue;
            }

            StringBuilder tagsToAppend = new StringBuilder();
            boolean willAppendAnything = false;

            for (String placeholder : placeholders) {
                if (placeholder == null || placeholder.isBlank()) {
                    continue;
                }
                ChartMapping mapping = placeholderMap.get(placeholder);
                if (mapping == null) {
                    continue;
                }

                String normalizedPlaceholder = normalizePlaceholderToken(placeholder);
                if (processedPlaceholders != null && processedPlaceholders.contains(normalizedPlaceholder)) {
                    continue;
                }

                // If placeholder already exists in section (even with whitespace/case
                // variants), skip.
                if (sectionContainsPlaceholder(sectionText, placeholder)) {
                    continue;
                }

                // If json block already exists in this section, mark as processed and skip.
                if (mapping.chartKey != null && sectionContainsJsonBlock(sectionText, mapping.chartKey)) {
                    if (processedPlaceholders != null) {
                        processedPlaceholders.add(normalizedPlaceholder);
                    }
                    continue;
                }

                // Only append if data exists and is not empty.
                if (chartsData == null || !chartsData.containsKey(mapping.dataKey)) {
                    logger.warn(
                            "addMissingChartsToSections: chartsData missing key '{}' for placeholder {} in roman section {}",
                            mapping.dataKey, placeholder, h.roman);
                    continue;
                }
                Object chartData = chartsData.get(mapping.dataKey);
                if (isEmptyChartData(chartData)) {
                    logger.warn(
                            "addMissingChartsToSections: chartData empty for key '{}' (placeholder {}) in roman section {}",
                            mapping.dataKey, placeholder, h.roman);
                    continue;
                }

                // Force append to the very end of this section.
                tagsToAppend.append("\n\n").append(placeholder).append("\n");
                willAppendAnything = true;
                if (processedPlaceholders != null) {
                    processedPlaceholders.add(normalizedPlaceholder);
                }
            }

            if (!willAppendAnything) {
                continue;
            }

            result = result.substring(0, sectionEnd) + tagsToAppend + result.substring(sectionEnd);
            logger.debug("addMissingChartsToSections: Added missing placeholder tags to roman section {}", h.roman);
        }

        return result;
    }

    private boolean sectionContainsPlaceholder(String sectionText, String canonicalPlaceholder) {
        if (sectionText == null || sectionText.isEmpty() || canonicalPlaceholder == null
                || canonicalPlaceholder.isBlank()) {
            return false;
        }
        // canonicalPlaceholder like [[CHART_MARKET_SHARE]]
        String inner = canonicalPlaceholder.replace("[[", "").replace("]]", "").trim();
        if (inner.isEmpty()) {
            return false;
        }
        String regex = "\\[\\[\\s*" + java.util.regex.Pattern.quote(inner) + "\\s*\\]\\]";
        return java.util.regex.Pattern.compile(regex, java.util.regex.Pattern.CASE_INSENSITIVE).matcher(sectionText)
                .find();
    }

    private boolean sectionContainsJsonBlock(String sectionText, String chartKey) {
        if (sectionText == null || sectionText.isEmpty() || chartKey == null || chartKey.isBlank()) {
            return false;
        }
        // Matches blocks like:
        // ```json:seasonality
        // {...}
        // ```
        String regex = "```\\s*json\\s*:\\s*" + java.util.regex.Pattern.quote(chartKey.trim()) + "\\b";
        return java.util.regex.Pattern.compile(regex, java.util.regex.Pattern.CASE_INSENSITIVE).matcher(sectionText)
                .find();
    }

    public MarketingAnalysisResult getAnalysisResult(String analysisId) {
        Optional<MarketingAnalysis> optAnalysis = repository.findById(analysisId);
        if (optAnalysis.isEmpty()) {
            return null;
        }

        MarketingAnalysis analysis = optAnalysis.get();
        MarketingAnalysisResult result = new MarketingAnalysisResult();
        result.setAnalysisId(analysis.getId());
        result.setStatus(analysis.getStatus());
        result.setCreatedAt(analysis.getCreatedAt());
        result.setCompletedAt(analysis.getCompletedAt());

        // Ищем связанный v2 анализ
        String v2AnalysisId = analysis.getV2AnalysisId();
        if (v2AnalysisId == null || v2AnalysisId.isEmpty()) {
            // Если v2AnalysisId не установлен, пытаемся найти по analysisId
            Optional<MarketingAnalysisV2Document> optV2 = v2Repository.findByAnalysisId(analysis.getId());
            if (optV2.isPresent()) {
                v2AnalysisId = optV2.get().getId();
                // Обновляем анализ для сохранения связи
                analysis.setV2AnalysisId(v2AnalysisId);
                repository.save(analysis);
            }
        }
        result.setV2AnalysisId(v2AnalysisId);

        if (analysis.getReportData() != null && "completed".equals(analysis.getStatus())) {
            MarketingAnalysisResult.MarketingReport report = buildReportFromData(analysis.getReportData(), analysis);
            result.setReport(report);
        }

        return result;
    }

    public Optional<MarketingAnalysis> getAnalysisById(String analysisId) {
        return repository.findById(analysisId);
    }

    public List<MarketingAnalysis> getUserAnalyses(String userId) {
        logger.debug("Fetching analyses for userId: {}", userId);
        try {
            // 1. Получаем стандартные анализы (V1 и те, что были созданы через
            // startAnalysis)
            List<MarketingAnalysis> v1Analyses = repository.findByUserIdOrderByCreatedAtDesc(userId);

            List<MarketingAnalysisV2Document> v2Analyses = v2Repository.findByUserId(userId);

            List<MarketingAnalysis> v2AsV1List = v2Analyses.stream()
                    // Фильтруем те V2, которые уже привязаны к V1 (чтобы не было дубликатов),
                    // если у вас есть логика связывания через analysisId
                    .filter(v2 -> v2.getAnalysisId() == null)
                    .map(v2 -> {
                        MarketingAnalysis ma = new MarketingAnalysis();
                        // Маппинг полей из V2 в V1
                        ma.setId(v2.getId()); // Используем ID документа V2
                        ma.setUserId(v2.getUserId());
                        ma.setProduct(v2.getProduct());
                        ma.setBusinessNiche(v2.getBusinessNiche());
                        ma.setGoal(v2.getGoal());
                        ma.setRegion(v2.getRegion());
                        ma.setTargetAudience(v2.getTargetAudience());
                        ma.setDetailLevel(v2.getDetailLevel());
                        ma.setAnalysisType(v2.getAnalysisType());

                        // V2 анализы, сохраненные через generateAndSaveAnalysisV2, считаются
                        // завершенными
                        ma.setStatus("completed");

                        // Важно: проставляем ссылку на самого себя или помечаем, что это V2
                        ma.setV2AnalysisId(v2.getId());

                        // Время создания (если в V2Document нет createdAt, берем текущее или из ID)
                        // Лучше добавить поле createdAt в MarketingAnalysisV2Document
                        if (v2.getCreatedAt() != null) {
                            ma.setCreatedAt(v2.getCreatedAt());
                            ma.setCompletedAt(v2.getCreatedAt());
                        } else {
                            // Fallback, если времени нет
                            ma.setCreatedAt(LocalDateTime.now());
                        }

                        return ma;
                    }).toList();

            List<MarketingAnalysis> allAnalyses = new ArrayList<>(v1Analyses);
            allAnalyses.addAll(v2AsV1List);

            allAnalyses.sort(Comparator.comparing(MarketingAnalysis::getCreatedAt,
                    Comparator.nullsLast(Comparator.reverseOrder())));

            logger.debug("Found {} total analyses (V1: {}, V2: {}) for userId: {}",
                    allAnalyses.size(), v1Analyses.size(), v2AsV1List.size(), userId);

            return allAnalyses;

        } catch (Exception e) {
            logger.error("Error fetching analyses for userId {}: {}", userId, e.getMessage(), e);
            throw e;
        }
    }

    private MarketingAnalysisResult.MarketingReport buildReportFromData(Map<String, Object> reportData,
            MarketingAnalysis analysis) {
        MarketingAnalysisResult.MarketingReport report = new MarketingAnalysisResult.MarketingReport();

        // Set summary (backward compatibility)
        report.setSummary((String) reportData.get("summary"));

        // Set full analysis (new field)
        String fullAnalysis = (String) reportData.get("fullAnalysis");
        if (fullAnalysis != null) {
            report.setFullAnalysis(fullAnalysis);
        }

        // Set charts data (new field)
        @SuppressWarnings("unchecked")
        Map<String, Object> chartsData = (Map<String, Object>) reportData.get("chartsData");
        if (chartsData != null) {
            report.setChartsData(chartsData);
        }

        if (analysis.getPdfFilename() != null) {
            report.setPdfUrl("/api/marketing/analysis/" + analysis.getId() + "/download");
        }

        return report;
    }

    /**
     * Оценивает примерный размер Map в байтах для логирования
     */
    private int estimateMapSize(Map<String, Object> map) {
        if (map == null) {
            return 0;
        }
        int size = 0;
        try {
            String json = objectMapper.writeValueAsString(map);
            size = json.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
        } catch (Exception e) {
            // Если не удалось сериализовать, используем приблизительную оценку
            size = map.size() * 100; // Примерно 100 байт на ключ
        }
        return size;
    }
}
