package kz.konturai.parser.service;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import kz.konturai.parser.dto.*;
import kz.konturai.parser.enums.*;
import kz.konturai.parser.model.MarketingAnalysisV3Document;
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class MarketingAnalysisV3Service {

    private final MarketingAnalysisV3Repository repository;
    private final SerperSearchService searchService;
    private final OpenAIAnalyticsService aiService;
    private final ObjectMapper objectMapper;

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

    // =====================================================================
    // PUBLIC API
    // =====================================================================

    public String createAndStartAnalysis(MarketingAnalysisV3Request request, String userId) {
        log.info("Creating new Analysis V3 for user: {}", userId);
        MarketingAnalysisV3Document doc = new MarketingAnalysisV3Document();
        doc.setUserId(userId);
        doc.setRequestData(request);
        doc.setStatus("QUEUED");
        doc.setCreatedAt(LocalDateTime.now());
        doc.setUpdatedAt(LocalDateTime.now());
        doc = repository.save(doc);
        log.info("Analysis document saved with ID: {}", doc.getId());
        processAnalysisAsync(doc.getId(), request);
        return doc.getId();
    }

    public Optional<MarketingAnalysisV3Document> getAnalysisById(String id) {
        return repository.findById(id);
    }

    public List<MarketingAnalysisV3Document> getAllByUser(String userId) {
        return repository.findAllByUserIdOrderByCreatedAtDesc(userId);
    }

    // =====================================================================
    // ASYNC PIPELINE
    // =====================================================================

    @Async("reportGenerationExecutor")
    public void processAnalysisAsync(String docId, MarketingAnalysisV3Request request) {
        try {
            log.info("[{}] Started async processing", docId);
            updateStatus(docId, "PROCESSING");

            // Step 1: Search
            log.info("[{}] Step 1/4: Deep Research", docId);
            Map<String, SerperSearchResult> searchResults = executeDeepResearch(request);
            saveResearchSnapshot(docId, searchResults);

            // Step 2: Digest — из SerperSearchResult извлекаем читаемый текст для AI
            log.info("[{}] Step 2/4: Digesting evidence", docId);
            String evidence = buildEvidence(searchResults, request);
            log.info("[{}] Evidence: {} chars, {} search blocks", docId, evidence.length(), searchResults.size());

            // Step 3: Prompts
            log.info("[{}] Step 3/4: Building prompts", docId);
            String systemPrompt = buildSystemPrompt(request);
            String userPrompt   = buildUserPrompt(request, evidence);

            // Step 4: AI generation
            log.info("[{}] Step 4/4: Calling AI model ({})", docId, highIntelligenceModel);
            String jsonResponse = generateWithRetry(docId, systemPrompt, userPrompt);

            log.info("[{}] Parsing result", docId);
            MarketingAnalysisV3Result result = parseAndFix(jsonResponse);

            completeAnalysis(docId, result);
            log.info("[{}] Analysis completed successfully", docId);

        } catch (Exception e) {
            log.error("[{}] FAILED: {}", docId, e.getMessage(), e);
            failAnalysis(docId, e.getMessage());
        }
    }

    // =====================================================================
    // STEP 1 — SEARCH
    // =====================================================================

    private Map<String, SerperSearchResult> executeDeepResearch(MarketingAnalysisV3Request request) {
        String niche   = sanitize(request.getBusinessNiche());
        String product = sanitize(request.getProductName());
        String topic   = (niche + " " + product).trim();
        if (topic.isBlank()) topic = sanitize(request.getProductDescription());

        List<String> cities = getTargetCities(request);
        String geo = String.join(" ", cities);

        // Простые конкретные запросы — Serper работает надёжнее без OR-site-нагромождений
        Map<String, String> queries = new LinkedHashMap<>();
        queries.put("market_overview",    topic + " рынок Казахстан 2024 2025 статистика тренды");
        queries.put("instagram_players",  topic + " " + geo + " Instagram аккаунты подписчики");
        queries.put("tiktok_players",     topic + " " + geo + " TikTok популярные блогеры");
        queries.put("google_maps",        topic + " " + geo + " рейтинг отзывы Google Maps 2GIS лучшие");
        queries.put("prices",             topic + " цены прайс Казахстан 2024 2025");
        queries.put("pain_points",        topic + " отзывы жалобы клиенты проблемы " + geo);
        queries.put("smm_cases",          niche + " SMM Instagram TikTok кейс Казахстан результаты");
        queries.put("search_demand",      topic + " Яндекс WordStat Казахстан частота сезонность");
        queries.put("competitor_content", topic + " контент стратегия конкуренты что публикуют");

        Map<String, SerperSearchResult> results = Collections.synchronizedMap(new LinkedHashMap<>());

        queries.entrySet().parallelStream().forEach(entry -> {
            String key   = entry.getKey();
            String query = entry.getValue();
            try {
                SerperSearchResult res = searchService.search(query, 10, "kz", "ru");
                log.info("[Search] '{}' → status={}, items={}",
                        key, res.status(), res.items() == null ? 0 : res.items().size());
                results.put(key, res);
            } catch (Exception e) {
                log.warn("[Search] '{}' failed: {}", key, e.getMessage());
                results.put(key, new SerperSearchResult(query, "ERROR", e.getMessage(), List.of()));
            }
        });

        return results;
    }

    // =====================================================================
    // STEP 2 — DIGEST
    // SerperSearchItem record: (title, link, snippet, sourceHost)
    // SerperSearchResult record: (query, status, error, items)
    // =====================================================================

    private String buildEvidence(Map<String, SerperSearchResult> searchResults,
                                 MarketingAnalysisV3Request request) {
        StringBuilder sb = new StringBuilder();
        sb.append("=== ДАННЫЕ ИЗ ПОИСКА (ОСНОВНОЙ ИСТОЧНИК ФАКТОВ) ===\n\n");
        sb.append("Ниша: ").append(sanitize(request.getBusinessNiche())).append("\n");
        sb.append("Продукт/услуга: ").append(sanitize(request.getProductName())).append("\n");
        sb.append("Целевые города: ").append(String.join(", ", getTargetCities(request))).append("\n\n");

        int totalSnippets = 0;

        for (Map.Entry<String, SerperSearchResult> entry : searchResults.entrySet()) {
            String key = entry.getKey();
            SerperSearchResult res = entry.getValue();

            sb.append("--- ").append(sectionLabel(key)).append(" ---\n");

            if (!"OK".equals(res.status()) || res.items() == null || res.items().isEmpty()) {
                sb.append("(данные недоступны для этого блока)\n\n");
                continue;
            }

            List<SerperSearchItem> items = res.items();
            int limit = Math.min(items.size(), 7);
            for (int i = 0; i < limit; i++) {
                SerperSearchItem item = items.get(i);
                // record: (title, link, snippet, sourceHost)
                String title   = item.title();
                String link    = item.link();
                String snippet = item.snippet();

                if ((title == null || title.isBlank()) && (snippet == null || snippet.isBlank())) continue;

                sb.append("  [").append(i + 1).append("] ");
                if (title   != null && !title.isBlank())   sb.append(title).append("\n      ");
                if (snippet != null && !snippet.isBlank()) sb.append(snippet).append("\n");
                if (link    != null && !link.isBlank())    sb.append("      Источник: ").append(link).append("\n");
                totalSnippets++;
            }
            sb.append("\n");
        }

        sb.append("=== ИТОГО ФАКТОВ ИЗ ПОИСКА: ").append(totalSnippets).append(" ===\n");
        if (totalSnippets == 0) {
            sb.append("ВНИМАНИЕ: Поиск не вернул данных. " +
                    "Генерируй оценки по методу Ферми на основе знаний о рынке Казахстана.\n");
        }

        return sb.toString();
    }

    private String sectionLabel(String key) {
        return switch (key) {
            case "market_overview"    -> "ОБЗОР РЫНКА";
            case "instagram_players"  -> "ИГРОКИ В INSTAGRAM";
            case "tiktok_players"     -> "ИГРОКИ В TIKTOK";
            case "google_maps"        -> "РЕЙТИНГИ И ОТЗЫВЫ (Google Maps / 2GIS)";
            case "prices"             -> "ЦЕНЫ";
            case "pain_points"        -> "БОЛИ КЛИЕНТОВ / ОТЗЫВЫ";
            case "smm_cases"          -> "SMM КЕЙСЫ";
            case "search_demand"      -> "ПОИСКОВЫЙ СПРОС";
            case "competitor_content" -> "КОНТЕНТ КОНКУРЕНТОВ";
            default                   -> key.toUpperCase();
        };
    }

    // =====================================================================
    // STEP 3 — PROMPTS
    // Все enum-значения читаем через .getDescription() для читаемого контекста
    // =====================================================================

    private String buildSystemPrompt(MarketingAnalysisV3Request request) {
        String niche  = sanitize(request.getBusinessNiche());
        String cities = String.join(", ", getTargetCities(request));

        return """
                Ты — старший маркетинговый аналитик рынка Казахстана и СНГ с 15-летним опытом.
                Твоя специализация: конкурентный анализ, SMM-стратегия, потребительское поведение.
                
                КОНТЕКСТ ПРОДУКТА:
                Этот анализ создаётся для платформы, которая АВТОМАТИЧЕСКИ заменяет SMM-специалиста для малого и среднего бизнеса Казахстана.
                Пользователи — обычные предприниматели, которые НЕ разбираются в маркетинге. Они хотят:
                1. Понять, что происходит на рынке прямо сейчас
                2. Узнать, кто их конкуренты и чем они сильны
                3. Получить чёткий план действий в соцсетях
                4. Видеть конкретные цифры, а не общие слова
                
                ТВОЯ РОЛЬ В ЭТОЙ СИСТЕМЕ:
                Ты генерируешь структурированный JSON-отчёт, который фронтенд превращает в красивый дашборд с графиками, таблицами и визуализациями. От качества твоего JSON зависит, насколько ценным окажется продукт для клиента.
                
                ═══════════════════════════════════════════════════
                ПРИНЦИПЫ РАБОТЫ (НАРУШАТЬ ЗАПРЕЩЕНО)
                ═══════════════════════════════════════════════════
                
                [П1] ПРИОРИТЕТ ИСТОЧНИКОВ:
                  1. Конкретные факты из блока "ДАННЫЕ ИЗ ПОИСКА" (названия, цифры, ссылки)
                  2. Твои знания о рынке Казахстана в данной нише
                  3. Метод Ферми — только если нет данных из п.1 и п.2
                
                [П2] РЕАЛИЗМ (физические ограничения рынка KZ):
                  • ER Instagram малый бизнес KZ: 1.5%% – 8.0%% (типично: 2-5%%)
                  • ER TikTok: 3.0%% – 15.0%%
                  • Рейтинг: шкала 1.0–5.0, типичный бизнес: 4.1–4.7
                  • Подписчики малый бизнес: 500–15,000 | средний: 15,000–100,000
                  • Посты/месяц: 6–20 (типично: 8–14)
                  • avgMonthlySearches узкая ниша: 300–5,000 | широкая: 8,000–80,000
                  • ciiIndex: целое число 0–100 СТРОГО
                  • densityIndex: дробное 0.0–100.0 СТРОГО
                  • starDistribution: сумма пяти значений = ровно 100.0
                  • radar-метрики: целые числа 0–100 СТРОГО
                  • Количество конкурентов по платформам: реалистичные числа, не 0
                
                [П3] КОНКРЕТНОСТЬ:
                  • Имена конкурентов — ТОЛЬКО реальные казахстанские бренды из поиска
                  • Если поиск не вернул имён — придумай ПРАВДОПОДОБНЫЕ казахстанские названия
                  • ЗАПРЕЩЕНО: "Компания А", "Бизнес 1", "Конкурент X"
                  • ОБЯЗАТЕЛЬНО: каждый конкурент должен иметь реалистичный профиль Instagram/TikTok
                
                [П4] ПОЛНОТА:
                  • NULL — ЗАПРЕЩЁН в любом поле
                  • Пустой массив [] или число 0 — допустимы только если это реальное значение
                  • Все текстовые поля — на русском языке
                  • keyFigures, contributingFactors, commonCta, relatedQueries — конкретные, с цифрами
                
                [П5] ПОНЯТНОСТЬ ДЛЯ НЕ-МАРКЕТОЛОГА:
                  • 10_structured_conclusions — пишутся языком бизнесмена, не маркетолога
                  • 11_smm_strategy_rationale — объясняй КАК БУДТО объясняешь клиенту лично, просто и с цифрами
                  • Каждый вывод должен отвечать на вопрос "что это значит для моего бизнеса?"
                
                ФОРМАТ ОТВЕТА: строго JSON объект. Никакого текста вне JSON. Никаких markdown-блоков.
                """.formatted(niche, niche, cities);
    }

    private String buildUserPrompt(MarketingAnalysisV3Request request, String evidence) {
        try {
            List<String> cities  = getTargetCities(request);

            // --- Читаем все enum-поля через .getDescription() ---
            String businessStageDesc = descOrEmpty(request.getBusinessStage());
            String clientTargetDesc  = descOrEmpty(request.getClientTarget());
            String offerTypeDesc     = descOrEmpty(request.getOfferType());
            String avgCheckDesc      = descOrEmpty(request.getAverageCheck());
            String geoScopeDesc      = descOrEmpty(request.getGeoScope());
            String purchaseFreqDesc  = descOrEmpty(request.getPurchaseFrequency());
            String visualFactorDesc  = descOrEmpty(request.getVisualFactor());
            String priceFeedbackDesc = descOrEmpty(request.getPriceFeedback());
            String smmStatusDesc     = descOrEmpty(request.getSmmStatus());
            String leadVolumeDesc    = descOrEmpty(request.getLeadVolume());
            String responseHandlerDesc = descOrEmpty(request.getResponseHandler());

            String customerBehaviorsDesc = listDesc(request.getCustomerBehaviors());
            String decisionPrioritiesDesc = listDesc(request.getDecisionPriorities());
            String discoveryMethodsDesc  = listDesc(request.getDiscoveryMethods());
            String constraintsDesc       = listDesc(request.getConstraints());

            // --- Позиционирование: зависит от smmStatus ---
            // SmmStatus.NONE означает "нет соцсетей" → BENCHMARK режим
            boolean hasSmmPresence = request.getSmmStatus() != null
                    && request.getSmmStatus() != SmmStatus.NONE;

            String positioningNote = !hasSmmPresence
                    ? "smmStatus=\"" + smmStatusDesc + "\" (нет активных соцсетей) → " +
                    "mode=\"BENCHMARK\", ВСЕ поля userMetrics = 0, marketMetrics = реальные рыночные данные"
                    : "smmStatus=\"" + smmStatusDesc + "\" (есть присутствие в соцсетях) → " +
                    "mode=\"COMPARISON\", userMetrics = оценка активности клиента на основе его статуса, " +
                    "marketMetrics = реальные рыночные данные";

            // --- Шаблоны городов — AI не сможет "забыть" ни один город ---
            String cityDistJson = cities.stream()
                    .map(c -> "\"" + c + "\": 0")
                    .collect(Collectors.joining(", ", "{", "}"));

            String cityCompJson = cities.stream()
                    .map(c -> "{\"city\": \"" + c + "\", \"activePlayers\": 0, " +
                            "\"avgEr\": 0.0, \"avgRating\": 0.0, \"avgPostsPerMonth\": 0}")
                    .collect(Collectors.joining(",\n        ", "[\n        ", "\n      ]"));

            return """
                    ЗАДАНИЕ: Сгенерируй полный маркетинговый анализ v4.0 для рынка Казахстана.

                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    ПРОФИЛЬ БИЗНЕСА КЛИЕНТА
                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    Ниша:                  %s
                    Продукт/Бренд:         %s
                    Цель анализа:          %s
                    Описание продукта:     %s
                    Этап бизнеса:          %s
                    Целевая аудитория:     %s
                    Тип предложения:       %s
                    Средний чек:           %s
                    Поведение клиентов:    %s

                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    ГЕОГРАФИЯ
                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    Охват:                 %s
                    Основной город:        %s
                    Города присутствия:    %s
                    Города продвижения:    %s

                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    ПОВЕДЕНИЕ И ЦЕННОСТИ КЛИЕНТОВ
                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    Частота покупки:       %s
                    Визуальный фактор:     %s
                    Приоритеты при выборе: %s
                    Как находят продукт:   %s
                    Ценовая реакция:       %s

                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    ТЕКУЩЕЕ СОСТОЯНИЕ БИЗНЕСА
                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    SMM статус:            %s
                    Объём лидов:           %s
                    Обработка обращений:   %s
                    Ограничения:           %s
                    Соцсети клиента:       %s
                    Известные конкуренты:  %s

                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    %s
                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    СПЕЦИАЛЬНЫЕ ТРЕБОВАНИЯ К БЛОКАМ
                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

                    [0_executive_summary]
                    • businessStage — опиши кратко этап: "Старт", "Рост", "Масштабирование", "Зрелость"
                    • keyFigures — ровно 6 фактов с цифрами, каждый начинается с эмодзи-маркера:
                      "📊 N активных конкурентов в нише по Instagram в {город}"
                      "⭐ Средний рейтинг ниши на картах: X.X из 5.0"
                      "📈 Тренд спроса: {направление} (+ X%% год к году)"
                      "🎯 Средний ER в нише: X.X%%"
                      "📅 Лидеры публикуют N постов/мес"
                      "💬 N%% бизнесов в нише имеют >50 отзывов"

                    [1_market_landscape]
                    • activePlayersByPlatform — укажи Instagram, TikTok, Telegram, GoogleMaps, TwoGis, YouTube
                    • cityDistribution — ОБЯЗАТЕЛЬНО все города из promotionCities: %s
                    • nicheReputationLevel — среднее доверие к нише: 0.0–5.0

                    [2_geo_structure]
                    • cityComparison — ОБЯЗАТЕЛЬНО все города: %s
                    • densityIndex — индекс конкурентной плотности 0–100:
                      0–30 = мало игроков, легко занять нишу
                      31–60 = средняя конкуренция
                      61–85 = высокая конкуренция
                      86–100 = рынок перегрет

                    [3_competitor_map]
                    • Минимум 7, максимум 10 конкурентов
                    • Используй РЕАЛЬНЫЕ названия из поиска — это критично
                    • Клиентские конкуренты из knownCompetitorLinks — включить ОБЯЗАТЕЛЬНО
                    • strengths: 3 конкретных сильных стороны (что они делают хорошо в SMM?)
                    • weaknesses: 3 конкретных слабости (что они делают плохо или не делают?)
                    • Пример слабости: "Нет видео-контента", "Не отвечают на комментарии", "Посты раз в 2 недели"
                    • Пример силы: "Reels каждые 3 дня с >5K просмотров", "Цены в каждом посте", "Stories каждый день"
                    • contentStyle — добавь дополнительное строковое поле: стиль контента конкурента
                    • lastActiveDate — строка: примерная дата последней активности "2024-01" или "активен"

                    [4_content_profile]
                    • Проценты ДОЛЖНЫ в сумме давать 100.0:
                      demoContentPercent + expertContentPercent + salesContentPercent +
                      reviewsContentPercent + engagementContentPercent = 100.0
                    • videoShare — доля видео от всего контента (Reels, TikTok, Stories)
                    • avgTextLengthCharacters — типичная длина подписи к посту в символах
                    • ctaUsagePercent — %% постов с явным призывом к действию
                    • topHashtags — добавь массив: 5 наиболее используемых хэштегов в нише
                    • bestPostingTime — добавь строку: лучшее время для публикации "18:00–21:00"

                    [5_competition_intensity]
                    • ciiIndex — вычисляется по формуле:
                      (кол-во игроков × 0.3) + (ср. ER × 5) + (ср. частота × 2) + (активность отзывов × 0.2)
                      Нормируй к шкале 0–100
                    • intensityLabel — одно из: "Низкая", "Ниже среднего", "Средняя", "Выше среднего", "Высокая", "Очень высокая"
                    • contributingFactors — 4 фактора с цифрами:
                      "Плотность: N активных аккаунтов в нише"
                      "Активность: средняя частота N постов/мес"
                      "Вовлечённость: средний ER X.X%%"
                      "Репутация: N%% бизнесов имеют рейтинг выше 4.5"

                    [6_reputation_analysis]
                    • starDistribution — сумма РОВНО 100.0
                    • highTrustBusinessShare — %% бизнесов с >100 отзывами И рейтингом >4.3
                    • avgOwnerResponseTimeHours — среднее время ответа владельца в часах (0 = не отвечает)
                    • topComplaintThemes — добавь массив строк: топ-3 жалобы клиентов в нише
                    • topPraiseThemes — добавь массив строк: топ-3 похвалы клиентов в нише

                    [7_behavioral_pattern]
                    • promoUsagePercent — %% постов конкурентов с акциями/скидками
                    • directBookingPercent — %% постов с призывом "записаться/заказать"
                    • priceVisibilityPercent — %% бизнесов, которые указывают цены открыто
                    • avgCycleDays — средний интервал между публикациями в днях
                    • commonCta — 5 реальных CTA-фраз из контента ниши (примеры из рынка KZ):
                      "Записывайтесь в Direct", "Цена в актуальном", "Звоните: +7..."

                    [8_search_demand]
                    • seasonality — 12 месяцев, значения нормированы к 100 (100 = пиковый месяц)
                    • peakPeriods — 2-3 конкретных периода: "Март–Апрель (pre-Наурыз)", "Ноябрь (Black Friday)"
                    • relatedQueries — 6 реальных поисковых запросов на русском, которые используют клиенты ниши
                    • trendDirection — добавь строку: "Растущий", "Стабильный", "Падающий"
                    • yoyGrowthPercent — добавь число: изменение спроса год-к-году в %%

                    [9_user_positioning]
                    • %s
                    • marketMetrics — реальные рыночные средние по нише (0–100):
                      activity: %% бизнесов, активно ведущих соцсети
                      engagement: нормированный средний ER (50 = средний ER ниши)
                      video: %% контента в формате видео
                      reputation: средний рейтинг нормированный (4.5 рейтинг = 90 из 100)
                      frequency: нормированная частота публикаций
                    • userMetrics — позиция клиента (0 если нет соцсетей)
                    • gap — добавь объект: разница marketMetrics - userMetrics по каждому параметру
                    • strategicFocus — 4 конкретных приоритета для клиента, отсортированных по важности

                    [10_structured_conclusions]
                    • РОВНО 7 выводов — с цифрами, языком для бизнесмена
                    • Формат каждого вывода: "[ТЕМА]: [ФАКТ с цифрой]. [ЧТО ЭТО ЗНАЧИТ для бизнеса]"

                    [11_smm_strategy_rationale]
                    • Минимум 120 слов
                    • Структура: ПОЧЕМУ SMM → ПОЧЕМУ ИМЕННО СЕЙЧАС → ЧТО КОНКРЕТНО ДЕЛАТЬ
                    • Пиши от первого лица, как консультант объясняет клиенту
                    • Обязательно упомяни: тип предложения "%s", аудиторию "%s", визуальный фактор "%s"
                    • Заканчивай конкретным следующим шагом: "Первый шаг — ..."

                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                    ПОЛНАЯ JSON СТРУКТУРА ДЛЯ ЗАПОЛНЕНИЯ
                    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

                    {
                      "0_executive_summary": {
                        "businessStage": "строка на русском",
                        "geography": "строка — перечисление городов",
                        "activeCompetitors": 0,
                        "competitionLevel": "низкий | средний | высокий | очень высокий",
                        "averageNicheEr": 0.0,
                        "averageRating": 0.0,
                        "demandTrend": "растущий | стабильный | падающий",
                        "keyFigures": [
                          "📊 N активных конкурентов в нише по Instagram",
                          "⭐ Средний рейтинг ниши: X.X из 5.0",
                          "📈 Тренд спроса: +X%% год к году",
                          "🎯 Средний ER в нише: X.X%%",
                          "📅 Лидеры публикуют N постов/мес",
                          "💬 N%% бизнесов имеют >50 отзывов"
                        ]
                      },
                      "1_market_landscape": {
                        "activePlayersByPlatform": {
                          "Instagram": 0,
                          "TikTok": 0,
                          "Telegram": 0,
                          "GoogleMaps": 0,
                          "TwoGis": 0,
                          "YouTube": 0
                        },
                        "cityDistribution": %s,
                        "nicheReputationLevel": 0.0
                      },
                      "2_geo_structure": {
                        "cityComparison": %s,
                        "densityIndex": 0.0
                      },
                      "3_competitor_map": [
                        {
                          "name": "Название бренда",
                          "platform": "Instagram",
                          "profileUrl": "https://instagram.com/...",
                          "followers": 0,
                          "postsPerMonth": 0,
                          "er": 0.0,
                          "rating": 0.0,
                          "reviews": 0,
                          "contentStyle": "описание стиля контента",
                          "lastActiveDate": "активен",
                          "strengths": ["сила 1 с цифрами", "сила 2 с цифрами", "сила 3"],
                          "weaknesses": ["слабость 1", "слабость 2", "слабость 3"]
                        }
                      ],
                      "4_content_profile": {
                        "demoContentPercent": 0.0,
                        "expertContentPercent": 0.0,
                        "salesContentPercent": 0.0,
                        "reviewsContentPercent": 0.0,
                        "engagementContentPercent": 0.0,
                        "videoShare": 0.0,
                        "avgTextLengthCharacters": 0,
                        "ctaUsagePercent": 0.0,
                        "topHashtags": ["#хэштег1", "#хэштег2", "#хэштег3", "#хэштег4", "#хэштег5"],
                        "bestPostingTime": "18:00–21:00"
                      },
                      "5_competition_intensity": {
                        "ciiIndex": 0,
                        "intensityLabel": "строка на русском",
                        "contributingFactors": [
                          "Плотность: N активных аккаунтов",
                          "Активность: ср. N постов/мес",
                          "Вовлечённость: ср. ER X.X%%",
                          "Репутация: N%% с рейтингом >4.5"
                        ]
                      },
                      "6_reputation_analysis": {
                        "avgNicheRating": 0.0,
                        "medianReviews": 0,
                        "starDistribution": {
                          "5": 0.0, "4": 0.0, "3": 0.0, "2": 0.0, "1": 0.0
                        },
                        "highTrustBusinessShare": 0.0,
                        "avgOwnerResponseTimeHours": 0,
                        "topComplaintThemes": ["жалоба 1", "жалоба 2", "жалоба 3"],
                        "topPraiseThemes": ["похвала 1", "похвала 2", "похвала 3"]
                      },
                      "7_behavioral_pattern": {
                        "promoUsagePercent": 0.0,
                        "directBookingPercent": 0.0,
                        "priceVisibilityPercent": 0.0,
                        "avgCycleDays": 0,
                        "commonCta": [
                          "CTA фраза 1",
                          "CTA фраза 2",
                          "CTA фраза 3",
                          "CTA фраза 4",
                          "CTA фраза 5"
                        ]
                      },
                      "8_search_demand": {
                        "avgMonthlySearches": 0,
                        "trendDirection": "Растущий | Стабильный | Падающий",
                        "yoyGrowthPercent": 0.0,
                        "seasonality": [
                          {"period": "Январь",   "value": 0.0},
                          {"period": "Февраль",  "value": 0.0},
                          {"period": "Март",     "value": 0.0},
                          {"period": "Апрель",   "value": 0.0},
                          {"period": "Май",      "value": 0.0},
                          {"period": "Июнь",     "value": 0.0},
                          {"period": "Июль",     "value": 0.0},
                          {"period": "Август",   "value": 0.0},
                          {"period": "Сентябрь", "value": 0.0},
                          {"period": "Октябрь",  "value": 0.0},
                          {"period": "Ноябрь",   "value": 0.0},
                          {"period": "Декабрь",  "value": 0.0}
                        ],
                        "peakPeriods": ["Период 1 с объяснением", "Период 2 с объяснением"],
                        "relatedQueries": [
                          "запрос 1", "запрос 2", "запрос 3",
                          "запрос 4", "запрос 5", "запрос 6"
                        ]
                      },
                      "9_user_positioning": {
                        "marketMetrics": {
                          "activity": 0,
                          "engagement": 0,
                          "video": 0,
                          "reputation": 0,
                          "frequency": 0
                        },
                        "userMetrics": {
                          "activity": 0,
                          "engagement": 0,
                          "video": 0,
                          "reputation": 0,
                          "frequency": 0
                        },
                        "gap": {
                          "activity": 0,
                          "engagement": 0,
                          "video": 0,
                          "reputation": 0,
                          "frequency": 0
                        },
                        "mode": "BENCHMARK | COMPARISON",
                        "strategicFocus": [
                          "Приоритет 1: конкретное действие",
                          "Приоритет 2: конкретное действие",
                          "Приоритет 3: конкретное действие",
                          "Приоритет 4: конкретное действие"
                        ]
                      },
                      "10_structured_conclusions": [
                        "КОНКУРЕНЦИЯ: факт. Что это значит.",
                        "КОНТЕНТ: факт. Что это значит.",
                        "РЕПУТАЦИЯ: факт. Что это значит.",
                        "СПРОС: факт. Что это значит.",
                        "ЦЕНЫ: факт. Что это значит.",
                        "ВИДЕО: факт. Что это значит.",
                        "ВОЗМОЖНОСТЬ: факт. Что это значит."
                      ],
                      "11_smm_strategy_rationale": "Минимум 120 слов. ПОЧЕМУ SMM → ПОЧЕМУ СЕЙЧАС → ЧТО ДЕЛАТЬ → ПЕРВЫЙ ШАГ."
                    }

                    ОТВЕТ: только JSON. Никакого текста вне JSON. Никаких markdown-блоков ```json.
                    """.formatted(
                    sanitize(request.getBusinessNiche()),
                    sanitize(request.getProductName()),
                    sanitize(request.getGoal()),
                    sanitize(request.getProductDescription()),
                    businessStageDesc,
                    clientTargetDesc,
                    offerTypeDesc,
                    avgCheckDesc,
                    customerBehaviorsDesc,
                    geoScopeDesc,
                    sanitize(request.getMainCity()),
                    String.join(", ", safeList(request.getPresenceCities())),
                    String.join(", ", cities),
                    purchaseFreqDesc,
                    visualFactorDesc,
                    decisionPrioritiesDesc,
                    discoveryMethodsDesc,
                    priceFeedbackDesc,
                    smmStatusDesc,
                    leadVolumeDesc,
                    responseHandlerDesc,
                    constraintsDesc,
                    String.join(", ", safeList(request.getUserSocialLinks())),
                    String.join(", ", safeList(request.getKnownCompetitorLinks())),
                    evidence,
                    cityDistJson,
                    cityCompJson,
                    positioningNote,
                    offerTypeDesc,
                    clientTargetDesc,
                    visualFactorDesc,
                    cityDistJson,
                    cityCompJson
            );
        } catch (Exception e) {
            throw new RuntimeException("Failed to build user prompt", e);
        }
    }

    // =====================================================================
    // STEP 4 — AI CALL WITH RETRY
    // =====================================================================

    private String generateWithRetry(String docId, String systemPrompt, String userPrompt) {
        int maxAttempts = 3;
        String lastError = "";

        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                log.info("[{}] AI attempt {}/{}", docId, attempt, maxAttempts);

                String raw = aiService.generateWithInstructionWithModel(
                        "{}", userPrompt, "ru", highIntelligenceModel,
                        systemPrompt, 16000, 240_000L
                );

                if (raw == null || raw.isBlank()) {
                    lastError = "Empty response from AI";
                    log.warn("[{}] Attempt {} — empty response", docId, attempt);
                    sleep(3000L * attempt);
                    continue;
                }

                String cleaned = extractJson(raw);
                if (cleaned == null || !cleaned.startsWith("{") || !cleaned.endsWith("}")) {
                    lastError = "Response is not a valid JSON object";
                    log.warn("[{}] Attempt {} — bad JSON structure, starts with: {}",
                            docId, attempt,
                            cleaned != null ? cleaned.substring(0, Math.min(60, cleaned.length())) : "null");
                    sleep(3000L * attempt);
                    continue;
                }

                // Финальная проверка парсируемости
                objectMapper.readTree(cleaned);
                log.info("[{}] Attempt {} — valid JSON ({} chars)", docId, attempt, cleaned.length());
                return cleaned;

            } catch (Exception e) {
                lastError = e.getMessage();
                log.warn("[{}] Attempt {} — error: {}", docId, attempt, e.getMessage());
                sleep(3000L * attempt);
            }
        }

        throw new RuntimeException("AI failed after " + maxAttempts + " attempts. Last error: " + lastError);
    }

    private String extractJson(String raw) {
        if (raw == null || raw.isBlank()) return null;
        String s = raw.trim();
        if (s.startsWith("```json")) s = s.substring(7);
        else if (s.startsWith("```")) s = s.substring(3);
        if (s.endsWith("```")) s = s.substring(0, s.length() - 3);
        s = s.trim();
        int first = s.indexOf('{');
        int last  = s.lastIndexOf('}');
        if (first >= 0 && last > first) return s.substring(first, last + 1);
        return s;
    }

    // =====================================================================
    // PARSE + POST-PROCESS FIX
    // =====================================================================

    private MarketingAnalysisV3Result parseAndFix(String json) throws Exception {
        ObjectMapper mapper = objectMapper.copy()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        MarketingAnalysisV3Result r = mapper.readValue(json, MarketingAnalysisV3Result.class);
        fixConstraints(r);
        return r;
    }

    private void fixConstraints(MarketingAnalysisV3Result r) {
        if (r == null) throw new IllegalStateException("Parsed result is null");

        if (r.getExecutiveSummary() != null) {
            var es = r.getExecutiveSummary();
            if (es.getAverageNicheEr() > 15.0) es.setAverageNicheEr(es.getAverageNicheEr() / 100.0);
            es.setAverageNicheEr(clamp(es.getAverageNicheEr(), 0.0, 100.0));
            es.setAverageRating(clamp(es.getAverageRating(), 0.0, 5.0));
        }

        if (r.getCompetitorMap() != null) {
            for (var cp : r.getCompetitorMap()) {
                if (cp.getEr() > 15.0) cp.setEr(cp.getEr() / 100.0);
                cp.setEr(clamp(cp.getEr(), 0.0, 100.0));
                cp.setRating(clamp(cp.getRating(), 0.0, 5.0));
                if (cp.getFollowers()     < 0) cp.setFollowers(0);
                if (cp.getPostsPerMonth() < 0) cp.setPostsPerMonth(0);
                if (cp.getReviews()       < 0) cp.setReviews(0);
                if (cp.getProfileUrl()    == null) cp.setProfileUrl("");
                if (cp.getContentStyle()  == null) cp.setContentStyle("");
                if (cp.getLastActiveDate()== null) cp.setLastActiveDate("");
            }
        }

        // Нормализация суммы контент-профиля к ровно 100.0
        if (r.getContentProfile() != null) {
            var cp = r.getContentProfile();
            double total = cp.getDemoContentPercent() + cp.getExpertContentPercent()
                         + cp.getSalesContentPercent() + cp.getReviewsContentPercent()
                         + cp.getEngagementContentPercent();
            if (total > 0 && Math.abs(total - 100.0) > 1.0) {
                double factor = 100.0 / total;
                cp.setDemoContentPercent(     round1(cp.getDemoContentPercent()      * factor));
                cp.setExpertContentPercent(   round1(cp.getExpertContentPercent()    * factor));
                cp.setSalesContentPercent(    round1(cp.getSalesContentPercent()     * factor));
                cp.setReviewsContentPercent(  round1(cp.getReviewsContentPercent()   * factor));
                // Последний элемент добирает остаток до ровно 100
                double rest = 100.0 - cp.getDemoContentPercent() - cp.getExpertContentPercent()
                                    - cp.getSalesContentPercent() - cp.getReviewsContentPercent();
                cp.setEngagementContentPercent(Math.max(0.0, round1(rest)));
            }
        }

        if (r.getReputationAnalysis() != null) {
            var ra = r.getReputationAnalysis();
            ra.setAvgNicheRating(clamp(ra.getAvgNicheRating(), 0.0, 5.0));
            ra.setHighTrustBusinessShare(clamp(ra.getHighTrustBusinessShare(), 0.0, 100.0));
            if (ra.getAvgOwnerResponseTimeHours() < 0) ra.setAvgOwnerResponseTimeHours(0);
            if (ra.getStarDistribution() != null && !ra.getStarDistribution().isEmpty()) {
                double sum = ra.getStarDistribution().values().stream().mapToDouble(Double::doubleValue).sum();
                if (sum > 0 && Math.abs(sum - 100.0) > 0.5) {
                    final double fSum = sum;
                    ra.getStarDistribution().replaceAll((k, v) ->
                            Math.round((v / fSum * 100.0) * 10.0) / 10.0);
                }
            }
            if (ra.getTopComplaintThemes() == null) ra.setTopComplaintThemes(List.of());
            if (ra.getTopPraiseThemes()    == null) ra.setTopPraiseThemes(List.of());
        }

        if (r.getCompetitionIntensity() != null) {
            r.getCompetitionIntensity().setCiiIndex(
                    (int) clamp(r.getCompetitionIntensity().getCiiIndex(), 0, 100));
        }

        if (r.getGeoStructure() != null) {
            r.getGeoStructure().setDensityIndex(
                    clamp(r.getGeoStructure().getDensityIndex(), 0.0, 100.0));
        }

        // Авто-вычисление gap и нормализация radar-метрик
        if (r.getUserPositioning() != null) {
            var up = r.getUserPositioning();
            fixRadar(up.getMarketMetrics());
            fixRadar(up.getUserMetrics());
            if (up.getGap() == null) up.setGap(new MarketingAnalysisV3Result.RadarMetrics());
            if (up.getMarketMetrics() != null && up.getUserMetrics() != null) {
                var gap = up.getGap();
                var m   = up.getMarketMetrics();
                var u   = up.getUserMetrics();
                gap.setActivity(  m.getActivity()   - u.getActivity());
                gap.setEngagement(m.getEngagement() - u.getEngagement());
                gap.setVideo(     m.getVideo()      - u.getVideo());
                gap.setReputation(m.getReputation() - u.getReputation());
                gap.setFrequency( m.getFrequency()  - u.getFrequency());
            }
        }

        log.info("fixConstraints passed — result is physically valid");
    }

    private void fixRadar(MarketingAnalysisV3Result.RadarMetrics m) {
        if (m == null) return;
        m.setActivity(  (int) clamp(m.getActivity(),   0, 100));
        m.setEngagement((int) clamp(m.getEngagement(), 0, 100));
        m.setVideo(     (int) clamp(m.getVideo(),      0, 100));
        m.setReputation((int) clamp(m.getReputation(), 0, 100));
        m.setFrequency( (int) clamp(m.getFrequency(),  0, 100));
    }

    private double round1(double v) {
        return Math.round(v * 10.0) / 10.0;
    }

    private double clamp(double val, double min, double max) {
        return Math.max(min, Math.min(max, val));
    }

    // =====================================================================
    // DB HELPERS
    // =====================================================================

    private void updateStatus(String id, String status) {
        repository.findById(id).ifPresent(doc -> {
            doc.setStatus(status);
            doc.setUpdatedAt(LocalDateTime.now());
            repository.save(doc);
        });
    }

    private void saveResearchSnapshot(String id, Map<String, SerperSearchResult> results) {
        try {
            Map<String, Object> snapshot = new LinkedHashMap<>();
            results.forEach((key, res) -> {
                Map<String, Object> block = new LinkedHashMap<>();
                block.put("status",    res.status());
                block.put("query",     res.query());
                block.put("itemCount", res.items() == null ? 0 : res.items().size());
                if (res.items() != null) {
                    block.put("preview", res.items().stream().limit(3).map(i -> {
                        Map<String, String> m = new LinkedHashMap<>();
                        m.put("title",   i.title());
                        m.put("snippet", i.snippet());
                        m.put("link",    i.link());
                        return m;
                    }).collect(Collectors.toList()));
                }
                snapshot.put(key, block);
            });
            snapshot.put("generatedAt", LocalDateTime.now().toString());

            repository.findById(id).ifPresent(doc -> {
                doc.setResearchMetaData(snapshot);
                repository.save(doc);
            });
        } catch (Exception e) {
            log.warn("[{}] Failed to save research snapshot (non-critical): {}", id, e.getMessage());
        }
    }

    private void completeAnalysis(String id, MarketingAnalysisV3Result result) {
        repository.findById(id).ifPresent(doc -> {
            doc.setResultData(result);
            doc.setStatus("COMPLETED");
            doc.setUpdatedAt(LocalDateTime.now());
            repository.save(doc);
        });
    }

    private void failAnalysis(String id, String error) {
        repository.findById(id).ifPresent(doc -> {
            doc.setStatus("FAILED");
            doc.setErrorMessage(error);
            doc.setUpdatedAt(LocalDateTime.now());
            repository.save(doc);
        });
    }

    // =====================================================================
    // UTILS
    // =====================================================================

    /**
     * Безопасно читает .getDescription() у любого enum с этим методом.
     * Использует интерфейс-паттерн через лямбда — без кастов к конкретному типу.
     */
    private <T extends Enum<T>> String descOrEmpty(T enumValue) {
        if (enumValue == null) return "";
        // Все наши enum-ы имеют getDescription() — вызываем через reflection-free трюк
        try {
            return (String) enumValue.getClass().getMethod("getDescription").invoke(enumValue);
        } catch (Exception e) {
            return enumValue.name();
        }
    }

    /**
     * Читает список enum-значений и возвращает их description через запятую.
     */
    private <T extends Enum<T>> String listDesc(List<T> list) {
        if (list == null || list.isEmpty()) return "не указано";
        return list.stream()
                .map(this::descOrEmpty)
                .collect(Collectors.joining(", "));
    }

    private List<String> safeList(List<String> list) {
        return list != null ? list : List.of();
    }

    private String sanitize(String input) {
        if (input == null || input.isBlank()) return "";
        return input.trim().replaceAll("\\s+", " ");
    }

    private List<String> getTargetCities(MarketingAnalysisV3Request request) {
        if (request.getPromotionCities() != null && !request.getPromotionCities().isEmpty())
            return request.getPromotionCities();
        if (request.getPresenceCities() != null && !request.getPresenceCities().isEmpty())
            return request.getPresenceCities();
        return List.of("Казахстан");
    }

    private void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); }
    }
}