package kz.konturai.parser.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import kz.konturai.parser.dto.MarketingAnalysisV3Request;
import kz.konturai.parser.dto.MarketingAnalysisV3Result;
import kz.konturai.parser.dto.targeting.*;
import kz.konturai.parser.enums.StrategyModel;
import kz.konturai.parser.model.MarketingAnalysisV3Document;
import kz.konturai.parser.model.MarketingStrategy;
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
import kz.konturai.parser.repository.MarketingStrategyRepository;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * AI Targeting System — два независимых режима:
 *
 * 1. V3-LINKED  launchTargetingFromStrategy(strategyId)
 *    Принимает только strategyId → сам тянет стратегию и анализ из MongoDB →
 *    строит полноценную AI-рекомендацию таргетинга на основе опросника →
 *    публикует первый пост в Facebook.
 *
 * 2. STATELESS  generateAnalysis / generateStrategy / generateCreative
 *    Для React-виджета: принимает тему, возвращает структуры без БД.
 */
@Service
@RequiredArgsConstructor
public class AiTargetingSystemService {

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

    private final OpenAIAnalyticsService openAIAnalyticsService;
    private final ObjectMapper objectMapper;
    private final MarketingStrategyRepository strategyRepository;
    private final MarketingAnalysisV3Repository analysisV3Repository;
    private final FacebookPostingService facebookPostingService;
    private final MinIOService minIOService;

    @Value("${targeting.ai.audience-model:gpt-4o}")
    private String analysisModel;

    @Value("${targeting.ai.creatives-model:gpt-4o}")
    private String strategyModel;

    @Value("${targeting.ai.creatives-model:gpt-4o}")
    private String creativeModel;

    @Value("${targeting.ai.max-audience-tokens:2500}")
    private int analysisMaxTokens;

    @Value("${targeting.ai.max-creatives-tokens:2000}")
    private int strategyMaxTokens;

    @Value("${targeting.ai.max-creatives-tokens:2000}")
    private int creativeMaxTokens;

    // ═══════════════════════════════════════════════════════════════════════════
    // V3-LINKED: главный метод — только strategyId
    // ═══════════════════════════════════════════════════════════════════════════

    /**
     * Единая точка входа для запуска таргетинга.
     *
     * Принимает strategyId → находит стратегию → находит анализ →
     * AI строит полную рекомендацию по опроснику →
     * публикует первый пост из postCalendar в Facebook.
     *
     * Пользователь потом вручную вводит бюджет в рекламном кабинете.
     * AI рекомендует всё остальное: тип таргетинга, аудиторию, форматы, фазы, KPI.
     */
    public TargetingLaunchResultDto launchTargetingFromStrategy(String strategyId) {
        logger.info("Launching targeting for strategyId={}", strategyId);

        // 1. Загружаем стратегию
        MarketingStrategy strategy = strategyRepository.findById(strategyId)
                .orElseThrow(() -> new IllegalArgumentException(
                        "Стратегия не найдена: " + strategyId));

        if (!"completed".equalsIgnoreCase(strategy.getStatus())) {
            throw new IllegalStateException(
                    "Стратегия ещё не готова. Текущий статус: " + strategy.getStatus());
        }

        // 2. Загружаем анализ — всё необходимое уже в нём
        MarketingAnalysisV3Document analysis = analysisV3Repository
                .findById(strategy.getAnalysisId())
                .orElseThrow(() -> new IllegalArgumentException(
                        "Анализ не найден: " + strategy.getAnalysisId()));

        MarketingAnalysisV3Request req = analysis.getRequestData();
        MarketingAnalysisV3Result  res = analysis.getResultData();

        // 3. AI строит рекомендацию таргетинга на основе опросника + результатов анализа
        TargetingRecommendationDto recommendation =
                buildTargetingRecommendation(req, res, strategy);

        // 4. Публикуем первый пост из postCalendar
        PublishedPostDto publishedPost = publishFirstPost(strategy);

        // 5. Собираем финальный ответ
        return TargetingLaunchResultDto.builder()
                .strategyId(strategyId)
                .analysisId(strategy.getAnalysisId())
                .scoringModelName(strategy.getScoringModelName())
                .scoringModelTitle(strategy.getScoringModelTitle())
                .businessNiche(safeStr(req != null ? req.getBusinessNiche() : null))
                .goal(safeStr(req != null ? req.getGoal() : null))
                .targetingRecommendation(recommendation)
                .publishedPost(publishedPost)
                .message(buildSummaryMessage(publishedPost, recommendation))
                .build();
    }

    public TargetingRecommendationDto generateLinkedRecommendation(String strategyId) {
        MarketingStrategy strategy = strategyRepository.findById(strategyId)
                .orElseThrow(() -> new IllegalArgumentException("Стратегия не найдена: " + strategyId));
        MarketingAnalysisV3Document analysis = analysisV3Repository.findById(strategy.getAnalysisId())
                .orElseThrow(() -> new IllegalArgumentException("Анализ не найден: " + strategy.getAnalysisId()));
        return generateLinkedRecommendation(strategy, analysis);
    }

    public TargetingRecommendationDto generateLinkedRecommendation(
            MarketingStrategy strategy,
            MarketingAnalysisV3Document analysis) {
        if (strategy == null) {
            throw new IllegalArgumentException("Стратегия не должна быть null");
        }
        if (analysis == null) {
            throw new IllegalArgumentException("Анализ не должен быть null");
        }
        return buildTargetingRecommendation(
                analysis.getRequestData(),
                analysis.getResultData(),
                strategy);
    }

    // ── AI: строим рекомендацию таргетинга ───────────────────────────────────

    private TargetingRecommendationDto buildTargetingRecommendation(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {

        logger.info("Building AI targeting recommendation for niche={}",
                req != null ? req.getBusinessNiche() : "unknown");

        String systemPrompt = """
                Ты — Senior Performance Marketing Strategist с 15-летним опытом на рынках СНГ.
                Специализируешься на платном трафике: Facebook Ads, Instagram Ads, таргетинг.

                Твоя задача — на основе данных маркетингового опросника и результатов AI-анализа
                составить исчерпывающую рекомендацию по таргетингу.

                Правила:
                - Возвращай ТОЛЬКО валидный JSON, без markdown, без пояснений вне JSON.
                - Все текстовые значения внутри JSON — строго на русском языке.
                - Будь конкретным: указывай реальные интересы Facebook/Instagram, поведения, возраст.
                - Рекомендуй тип таргетинга исходя из поведения покупателей и стадии бизнеса.
                - Бюджеты указывай в тенге (KZT).
                """;

        systemPrompt = buildEnhancedTargetingSystemPrompt();
        String instruction = buildEnhancedTargetingPromptInstruction();
        String context = buildEnhancedFullContext(req, res, strategy);

        String raw = openAIAnalyticsService.generateWithInstructionWithModel(
                context, instruction, "ru", strategyModel,
                systemPrompt, 4500, 150000L);

        if (raw == null) {
            logger.warn("AI returned null for targeting recommendation, using fallback");
            return buildFallbackRecommendation(req, res, strategy);
        }

        try {
            TargetingRecommendationDto recommendation =
                    objectMapper.readValue(extractJson(raw), TargetingRecommendationDto.class);
            return validateAndNormalizeRecommendation(recommendation, req, res, strategy);
        } catch (Exception e) {
            logger.error("Failed to parse targeting recommendation", e);
            return buildFallbackRecommendation(req, res, strategy);
        }
    }

    private String buildTargetingPromptInstruction() {
        return """
                На основе ВСЕХ предоставленных данных опросника и анализа рынка
                составь профессиональную рекомендацию по таргетингу и верни строго этот JSON:

                {
                  "recommendedType": "COLD_INTEREST | BEHAVIORAL | LOOKALIKE | RETARGETING | LEAD_GEN | AWARENESS | MIXED",
                  "recommendedTypeLabel": "Читаемое название типа таргетинга",
                  "typeRationale": "2-3 предложения: почему именно этот тип, со ссылкой на данные опросника",

                  "campaignObjective": "AWARENESS | TRAFFIC | LEADS | ENGAGEMENT | SALES",
                  "campaignObjectiveLabel": "Читаемое название цели",
                  "campaignObjectiveRationale": "Обоснование выбора цели кампании",

                  "audienceProfile": {
                    "primarySegment": "Подробное описание основного сегмента",
                    "secondarySegment": "Описание дополнительного сегмента",
                    "ageRange": "например 28-44",
                    "gender": "например Женщины 65%, Мужчины 35%",
                    "interests": ["конкретный интерес FB/IG 1", "интерес 2", "интерес 3", "интерес 4", "интерес 5"],
                    "behaviors": ["поведенческий таргетинг 1", "поведение 2", "поведение 3"],
                    "geography": ["город 1", "город 2"],
                    "exclusions": ["исключение 1", "исключение 2"],
                    "language": "Русский, Казахский"
                  },

                  "recommendedPlatforms": ["Instagram", "Facebook"],
                  "recommendedAdFormats": ["Stories", "Reels", "Feed"],
                  "recommendedPlacements": ["Instagram Feed", "Instagram Stories", "Facebook Feed", "Audience Network"],

                  "budgetRecommendation": {
                    "dailyBudget": "например 10 000 – 20 000 KZT",
                    "monthlyBudget": "например 300 000 – 600 000 KZT",
                    "bidStrategy": "например Минимальная стоимость результата",
                    "rationale": "2 предложения почему такой бюджет",
                    "estimatedCpc": "например 80 – 200 KZT",
                    "estimatedCpl": "например 1 500 – 4 000 KZT",
                    "estimatedDailyReach": "например 8 000 – 25 000 человек/день"
                  },

                  "phases": [
                    {
                      "phase": 1,
                      "name": "Холодный охват",
                      "duration": "10 дней",
                      "targetingType": "COLD_INTEREST",
                      "objective": "цель фазы",
                      "audience": "кто видит рекламу в этой фазе",
                      "adFormats": ["Stories", "Reels"],
                      "budget": "30% от общего бюджета",
                      "kpi": "ожидаемый результат",
                      "tactics": ["тактика 1", "тактика 2"]
                    },
                    {
                      "phase": 2,
                      "name": "Вовлечение и прогрев",
                      "duration": "10 дней",
                      "targetingType": "BEHAVIORAL",
                      "objective": "...",
                      "audience": "...",
                      "adFormats": ["..."],
                      "budget": "40% от общего бюджета",
                      "kpi": "...",
                      "tactics": ["..."]
                    },
                    {
                      "phase": 3,
                      "name": "Конверсия",
                      "duration": "10 дней",
                      "targetingType": "RETARGETING",
                      "objective": "...",
                      "audience": "...",
                      "adFormats": ["..."],
                      "budget": "30% от общего бюджета",
                      "kpi": "...",
                      "tactics": ["..."]
                    }
                  ],

                  "estimatedCtr": "например 1.5 – 3%",
                  "estimatedCpl": "например 1 500 – 4 000 KZT",
                  "estimatedReach": "например 50 000 – 120 000 человек за 30 дней",
                  "estimatedFrequency": "например 2 – 3 раза в неделю на человека",
                  "estimatedConversions": "например 80 – 250 лидов",

                  "questionnaireSignals": [
                    "Сигнал из опросника 1 — почему он важен для таргетинга",
                    "Сигнал 2",
                    "Сигнал 3",
                    "Сигнал 4"
                  ],

                  "competitiveContext": "1-2 предложения о конкурентной среде и как это влияет на таргетинг",
                  "ciiLevel": "например Высокий (CII=72/100) — нужна дифференциация",
                  "differentiationAdvice": "Как выделиться в рекламе на фоне конкурентов",

                  "quickWins": [
                    "Быстрая победа 1 — что можно сделать немедленно",
                    "Быстрая победа 2",
                    "Быстрая победа 3"
                  ],
                  "warnings": [
                    "Предупреждение 1 — чего избегать",
                    "Предупреждение 2"
                  ],

                  "contentMix": [
                    "40% — демонстрационный контент",
                    "30% — социальные доказательства",
                    "20% — экспертный контент",
                    "10% — прямые продажи"
                  ],
                  "postingFrequency": "например 1 раз в день в рабочие дни, 2 раза в выходные",
                  "bestPostingTimes": ["09:00", "13:00", "19:00 – 21:00"],

                  "primaryCta": "например «Записаться на бесплатную консультацию»",
                  "funnelStage": "например Середина воронки — прогрев и доверие",
                  "conversionMechanism": "например Lead Ads форма → WhatsApp → менеджер"
                }

                Данные для анализа:
                """;
    }

    /**
     * Собирает весь контекст: опросник + результаты анализа + параметры стратегии.
     * Это то, что AI получает как "данные для анализа".
     */
    private String buildFullContext(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {

        StringBuilder ctx = new StringBuilder();

        // ── БЛОК 1: Данные опросника ─────────────────────────────────────────
        ctx.append("═══ ДАННЫЕ ОПРОСНИКА (источник истины) ═══\n");

        if (req != null) {
            ctx.append("Ниша:                ").append(safeStr(req.getBusinessNiche())).append("\n");
            ctx.append("Продукт:             ").append(safeStr(req.getProductName())).append("\n");
            ctx.append("Цель бизнеса:        ").append(safeStr(req.getGoal())).append("\n");
            ctx.append("Описание продукта:   ").append(safeStr(req.getProductDescription())).append("\n\n");

            ctx.append("── Блок 1: О бизнесе ──\n");
            ctx.append("Стадия бизнеса:      ").append(describeEnum(req.getBusinessStage())).append("\n");
            ctx.append("Тип клиентов:        ").append(describeEnum(req.getClientTarget())).append("\n");
            ctx.append("Тип оффера:          ").append(describeEnum(req.getOfferType())).append("\n");
            ctx.append("Средний чек:         ").append(describeEnum(req.getAverageCheck())).append("\n");
            ctx.append("Поведение покупателей: ");
            if (req.getCustomerBehaviors() != null) {
                req.getCustomerBehaviors().forEach(b -> ctx.append(describeEnum(b)).append("; "));
            }
            ctx.append("\n\n");

            ctx.append("── Блок 2: География ──\n");
            ctx.append("Охват:               ").append(describeEnum(req.getGeoScope())).append("\n");
            ctx.append("Основной город:      ").append(safeStr(req.getMainCity())).append("\n");
            ctx.append("Города продвижения:  ").append(formatList(req.getPromotionCities())).append("\n\n");

            ctx.append("── Блок 3: Продукт и контент ──\n");
            ctx.append("Частота покупки:     ").append(describeEnum(req.getPurchaseFrequency())).append("\n");
            ctx.append("Визуальный фактор:   ").append(describeEnum(req.getVisualFactor())).append("\n\n");

            ctx.append("── Блок 4: Принятие решения ──\n");
            ctx.append("Приоритеты при выборе: ");
            if (req.getDecisionPriorities() != null) {
                req.getDecisionPriorities().forEach(d -> ctx.append(describeEnum(d)).append("; "));
            }
            ctx.append("\n");
            ctx.append("Как находят:         ");
            if (req.getDiscoveryMethods() != null) {
                req.getDiscoveryMethods().forEach(d -> ctx.append(describeEnum(d)).append("; "));
            }
            ctx.append("\n");
            ctx.append("Реакция на цену:     ").append(describeEnum(req.getPriceFeedback())).append("\n\n");

            ctx.append("── Блок 5: Текущая ситуация ──\n");
            ctx.append("SMM статус:          ").append(describeEnum(req.getSmmStatus())).append("\n");
            ctx.append("Объём лидов сейчас:  ").append(describeEnum(req.getLeadVolume())).append("\n");
            ctx.append("Кто обрабатывает:    ").append(describeEnum(req.getResponseHandler())).append("\n");
            if (req.getConstraints() != null && !req.getConstraints().isEmpty()) {
                ctx.append("Ограничения:         ");
                req.getConstraints().forEach(c -> ctx.append(describeEnum(c)).append("; "));
                ctx.append("\n");
            }
            ctx.append("\n");
        }

        // ── БЛОК 2: Результаты AI-анализа рынка ─────────────────────────────
        ctx.append("═══ РЕЗУЛЬТАТЫ AI-АНАЛИЗА РЫНКА ═══\n");

        if (res != null) {
            if (res.getExecutiveSummary() != null) {
                MarketingAnalysisV3Result.ExecutiveSummary es = res.getExecutiveSummary();
                ctx.append("Уровень конкуренции: ").append(safeStr(es.getCompetitionLevel())).append("\n");
                ctx.append("Тренд спроса:        ").append(safeStr(es.getDemandTrend())).append("\n");
                ctx.append("Активных конкурентов:").append(es.getActiveCompetitors()).append("\n");
                ctx.append("Средний ER ниши:     ").append(es.getAverageNicheEr()).append("%\n");
                if (es.getKeyFigures() != null && !es.getKeyFigures().isEmpty()) {
                    ctx.append("Ключевые цифры:      ").append(String.join("; ", es.getKeyFigures())).append("\n");
                }
                ctx.append("\n");
            }

            if (res.getCompetitionIntensity() != null) {
                MarketingAnalysisV3Result.CompetitionIntensity ci = res.getCompetitionIntensity();
                ctx.append("Индекс CII:          ").append(ci.getCiiIndex()).append("/100 — ")
                   .append(safeStr(ci.getIntensityLabel())).append("\n");
                if (ci.getContributingFactors() != null && !ci.getContributingFactors().isEmpty()) {
                    ctx.append("Факторы конкуренции: ").append(String.join("; ", ci.getContributingFactors())).append("\n");
                }
                ctx.append("\n");
            }

            if (res.getSearchDemand() != null) {
                MarketingAnalysisV3Result.SearchDemand sd = res.getSearchDemand();
                ctx.append("Поисковый спрос/мес: ").append(sd.getAvgMonthlySearches()).append(" запросов\n");
                ctx.append("Тренд спроса:        ").append(safeStr(sd.getTrendDirection())).append("\n\n");
            }

            if (res.getContentProfile() != null) {
                MarketingAnalysisV3Result.ContentProfile cp = res.getContentProfile();
                ctx.append("Контент конкурентов:\n");
                ctx.append("  Демо-контент:   ").append(cp.getDemoContentPercent()).append("%\n");
                ctx.append("  Экспертный:     ").append(cp.getExpertContentPercent()).append("%\n");
                ctx.append("  Продажный:      ").append(cp.getSalesContentPercent()).append("%\n");
                ctx.append("  Отзывы:         ").append(cp.getReviewsContentPercent()).append("%\n");
                ctx.append("  Доля видео:     ").append(cp.getVideoShare()).append("%\n");
                ctx.append("  Лучшее время:   ").append(safeStr(cp.getBestPostingTime())).append("\n");
                if (cp.getTopHashtags() != null && !cp.getTopHashtags().isEmpty()) {
                    ctx.append("  Топ хештеги:  ").append(String.join(", ", cp.getTopHashtags())).append("\n");
                }
                ctx.append("\n");
            }

            if (res.getBehavioralPattern() != null) {
                MarketingAnalysisV3Result.BehavioralPattern bp = res.getBehavioralPattern();
                ctx.append("Поведенческий паттерн:\n");
                ctx.append("  Используют акции:  ").append(bp.getPromoUsagePercent()).append("%\n");
                ctx.append("  Прямая запись:     ").append(bp.getDirectBookingPercent()).append("%\n");
                ctx.append("  Цикл принятия:     ").append(bp.getAvgCycleDays()).append(" дней\n\n");
            }

            if (res.getReputationAnalysis() != null) {
                MarketingAnalysisV3Result.ReputationAnalysis ra = res.getReputationAnalysis();
                ctx.append("Репутация в нише:    ").append(ra.getAvgNicheRating()).append(" ★\n");
                if (ra.getTopComplaintThemes() != null && !ra.getTopComplaintThemes().isEmpty()) {
                    ctx.append("Топ жалобы:          ").append(String.join("; ", ra.getTopComplaintThemes())).append("\n");
                }
                ctx.append("\n");
            }

            if (res.getStructuredConclusions() != null && !res.getStructuredConclusions().isEmpty()) {
                ctx.append("Выводы анализа:\n");
                res.getStructuredConclusions().forEach(c -> ctx.append("  • ").append(c).append("\n"));
                ctx.append("\n");
            }

            if (res.getSmmStrategyRationale() != null && !res.getSmmStrategyRationale().isBlank()) {
                ctx.append("SMM-ратионале:       ").append(res.getSmmStrategyRationale()).append("\n\n");
            }
        }

        // ── БЛОК 3: Параметры стратегии ──────────────────────────────────────
        ctx.append("═══ ПАРАМЕТРЫ СТРАТЕГИИ ═══\n");
        ctx.append("Модель стратегии:    ")
           .append(safeStr(strategy.getScoringModelTitle())).append("\n");
        ctx.append("Статус:              ").append(safeStr(strategy.getStatus())).append("\n");
        if (strategy.getPriorityPlatforms() != null && !strategy.getPriorityPlatforms().isEmpty()) {
            ctx.append("Приоритетные платформы: ")
               .append(String.join(", ", strategy.getPriorityPlatforms())).append("\n");
        }
        ctx.append("Длительность:        ")
           .append(safeStr(String.valueOf(strategy.getDurationWeeks()))).append(" недель\n");

        return ctx.toString();
    }

    private String buildEnhancedTargetingSystemPrompt() {
        return """
                You are NOT just a generator. You are a Senior Performance Architect with 20 years of experience in paid acquisition,
                funnel strategy, landing page diagnostics, Meta Ads scaling, and growth systems.
                Your recommendations must be surgical, commercially sharp, aggressive for growth, but cautious about technical health
                and burn-out risk. You speak like a real performance lead and use concepts such as LTV, ROAS, CPL, CAC,
                Attribution Window, Retargeting windows, budget pacing, creative fatigue, and conversion friction.

                Rules:
                - Return valid JSON only.
                - Every narrative field inside JSON must be in Russian.
                - Keep recommendations practical and compatible with real paid traffic operations.
                - Budget values must stay in KZT.
                - Do not omit mandatory sections even if data is limited.
                """;
    }

    private String buildEnhancedTargetingPromptInstruction() {
        return """
                На основе всех данных опросника, анализа рынка, текущих digital assets, сильных сторон конкурентов и модели стратегии
                верни только валидный JSON без markdown и без пояснений вне JSON.

                Правила:
                - Все текстовые поля на русском языке.
                - Используй профессиональную performance-терминологию: ROAS, CPL, CTR, LTV, CAC, Pixel, CAPI, GTM, Retargeting.
                - Не пропускай ни один обязательный блок.
                - hypothesisMatrix обязан содержать ровно 4 rows, 4 cols и 16 cells.
                - lpAudit.matchScore должен быть числом от 0 до 100.
                - suggestedRules должен содержать 3 реалистичных automation rules.
                - Funnel strategy обязана разделять холодную, теплую и горячую аудиторию.
                - LP audit должен указывать 3 типичных conversion killers ниши.
                - Risk analysis обязан учитывать policy risk и creative fatigue.

                Верни JSON со всеми существующими полями recommendation и обязательными новыми блоками:
                pixelHealth, funnelStrategy, hypothesisMatrix, lpAudit, suggestedRules, scalingPlan, risks.
                """;
    }

    private String buildEnhancedFullContext(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        StringBuilder ctx = new StringBuilder(buildFullContext(req, res, strategy));
        appendSocialLinksContext(ctx, req);
        appendCompetitorStrengthsContext(ctx, res);
        appendCompetitiveInterpretation(ctx, req, res);
        appendStrategyModelContext(ctx, strategy);
        return ctx.toString();
    }

    private void appendSocialLinksContext(StringBuilder ctx, MarketingAnalysisV3Request req) {
        if (req == null || req.getUserSocialLinks() == null || req.getUserSocialLinks().isEmpty()) {
            return;
        }
        ctx.append("\n=== CURRENT DIGITAL ASSETS ===\n");
        req.getUserSocialLinks().stream()
                .filter(Objects::nonNull)
                .map(String::trim)
                .filter(link -> !link.isBlank())
                .forEach(link -> ctx.append("- ").append(link).append("\n"));
    }

    private void appendCompetitorStrengthsContext(StringBuilder ctx, MarketingAnalysisV3Result res) {
        List<String> strengths = extractTopCompetitorStrengths(res);
        if (strengths.isEmpty()) {
            return;
        }
        ctx.append("\n=== TOP COMPETITOR STRENGTHS ===\n");
        strengths.forEach(strength -> ctx.append("- ").append(strength).append("\n"));
    }

    private List<String> extractTopCompetitorStrengths(MarketingAnalysisV3Result res) {
        if (res == null || res.getCompetitorMap() == null) {
            return List.of();
        }
        return res.getCompetitorMap().stream()
                .filter(Objects::nonNull)
                .sorted(Comparator
                        .comparingInt((MarketingAnalysisV3Result.CompetitorProfile profile) ->
                                profile.getStrengths() != null ? profile.getStrengths().size() : 0)
                        .reversed()
                        .thenComparingInt(MarketingAnalysisV3Result.CompetitorProfile::getFollowers).reversed())
                .limit(3)
                .flatMap(profile -> {
                    List<String> strengths = profile.getStrengths();
                    if (strengths == null || strengths.isEmpty()) {
                        return List.<String>of().stream();
                    }
                    return strengths.stream()
                            .filter(Objects::nonNull)
                            .map(String::trim)
                            .filter(s -> !s.isBlank())
                            .map(s -> safeStr(profile.getName()) + ": " + s);
                })
                .distinct()
                .collect(Collectors.toList());
    }

    private void appendCompetitiveInterpretation(
            StringBuilder ctx,
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res) {
        if (res == null || res.getCompetitionIntensity() == null) {
            return;
        }
        int cii = res.getCompetitionIntensity().getCiiIndex();
        ctx.append("\n=== PERFORMANCE INTERPRETATION ===\n");
        if (cii >= 70) {
            ctx.append("- Нужна агрессивная differentiation strategy и жесткий контроль creative fatigue.\n");
        } else if (cii >= 40) {
            ctx.append("- Нужен баланс между дифференциацией и cost-efficient закупкой.\n");
        } else {
            ctx.append("- Можно активнее тестировать холодный трафик и более широкий охват.\n");
        }
        if (req != null && req.getAverageCheck() != null && "HIGH".equalsIgnoreCase(req.getAverageCheck().name())) {
            ctx.append("- Высокий чек: усиливаем trust layer, BOF и качественную аргументацию оффера.\n");
        }
        if (res.getContentProfile() != null) {
            ctx.append("- Доля video content у конкурентов: ")
                    .append(res.getContentProfile().getVideoShare())
                    .append("%.\n");
        }
    }

    private void appendStrategyModelContext(StringBuilder ctx, MarketingStrategy strategy) {
        if (strategy == null) {
            return;
        }
        ctx.append("\n=== STRATEGY MODEL CONTEXT ===\n");
        ctx.append("- scoringModelName: ").append(safeStr(strategy.getScoringModelName())).append("\n");
        ctx.append("- scoringModelTitle: ").append(safeStr(strategy.getScoringModelTitle())).append("\n");
    }

    private TargetingRecommendationDto validateAndNormalizeRecommendation(
            TargetingRecommendationDto recommendation,
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        TargetingRecommendationDto base = buildFallbackRecommendation(req, res);
        TargetingRecommendationDto normalized = recommendation != null ? recommendation : base;

        if (isBlank(normalized.getRecommendedType())) normalized.setRecommendedType(base.getRecommendedType());
        if (isBlank(normalized.getRecommendedTypeLabel())) normalized.setRecommendedTypeLabel(base.getRecommendedTypeLabel());
        if (isBlank(normalized.getTypeRationale())) normalized.setTypeRationale(base.getTypeRationale());
        if (isBlank(normalized.getCampaignObjective())) normalized.setCampaignObjective(base.getCampaignObjective());
        if (isBlank(normalized.getCampaignObjectiveLabel())) normalized.setCampaignObjectiveLabel(base.getCampaignObjectiveLabel());
        if (isBlank(normalized.getCampaignObjectiveRationale())) normalized.setCampaignObjectiveRationale(base.getCampaignObjectiveRationale());
        if (normalized.getAudienceProfile() == null) normalized.setAudienceProfile(base.getAudienceProfile());
        if (normalized.getBudgetRecommendation() == null) normalized.setBudgetRecommendation(base.getBudgetRecommendation());
        if (normalized.getRecommendedPlatforms() == null || normalized.getRecommendedPlatforms().isEmpty()) {
            normalized.setRecommendedPlatforms(base.getRecommendedPlatforms());
        }
        if (normalized.getRecommendedAdFormats() == null || normalized.getRecommendedAdFormats().isEmpty()) {
            normalized.setRecommendedAdFormats(base.getRecommendedAdFormats());
        }
        if (normalized.getQuickWins() == null || normalized.getQuickWins().isEmpty()) {
            normalized.setQuickWins(base.getQuickWins());
        }
        if (normalized.getWarnings() == null || normalized.getWarnings().isEmpty()) {
            normalized.setWarnings(base.getWarnings());
        }

        normalized.setQuestionnaireSignals(normalizeTextList(
                normalized.getQuestionnaireSignals(),
                List.of(
                        "Покупатель принимает решение не мгновенно и требует прогрева.",
                        "Сегменту важна понятная ценность оффера до клика.",
                        "Конкурентная среда влияет на стоимость лида и частоту тестов.",
                        "Таргетинг должен опираться на реальный customer behavior, а не на абстрактные интересы."
                ),
                4));

        normalized.setRecommendedPlacements(normalizeTextList(
                normalized.getRecommendedPlacements(),
                List.of("Instagram Feed", "Instagram Stories", "Facebook Feed"),
                3));

        if (normalized.getPixelHealth() == null) {
            normalized.setPixelHealth(buildDefaultPixelHealth(req, res, strategy));
        } else {
            normalized.getPixelHealth().setSetupInstructions(truncateText(
                    defaultIfBlank(normalized.getPixelHealth().getSetupInstructions(),
                            buildDefaultPixelHealth(req, res, strategy).getSetupInstructions()),
                    600));
        }

        if (normalized.getFunnelStrategy() == null) {
            normalized.setFunnelStrategy(buildDefaultFunnelStrategy(req, res, strategy));
        } else {
            FunnelStrategyDto fallbackFunnel = buildDefaultFunnelStrategy(req, res, strategy);
            normalized.getFunnelStrategy().setTof(truncateText(defaultIfBlank(normalized.getFunnelStrategy().getTof(), fallbackFunnel.getTof()), 450));
            normalized.getFunnelStrategy().setMof(truncateText(defaultIfBlank(normalized.getFunnelStrategy().getMof(), fallbackFunnel.getMof()), 450));
            normalized.getFunnelStrategy().setBof(truncateText(defaultIfBlank(normalized.getFunnelStrategy().getBof(), fallbackFunnel.getBof()), 450));
        }

        normalized.setHypothesisMatrix(normalizeMatrix(normalized.getHypothesisMatrix(), req, res, strategy));

        if (normalized.getLpAudit() == null) {
            normalized.setLpAudit(buildDefaultLandingPageAudit(req, res, strategy));
        } else {
            LandingPageAuditDto fallbackAudit = buildDefaultLandingPageAudit(req, res, strategy);
            normalized.getLpAudit().setMatchScore(Math.max(0, Math.min(100, normalized.getLpAudit().getMatchScore())));
            normalized.getLpAudit().setExecutiveSummary(truncateText(
                    defaultIfBlank(normalized.getLpAudit().getExecutiveSummary(), fallbackAudit.getExecutiveSummary()),
                    500));
            normalized.getLpAudit().setCriticalFixes(normalizeTextList(
                    normalized.getLpAudit().getCriticalFixes(),
                    fallbackAudit.getCriticalFixes(),
                    3));
            normalized.getLpAudit().setTrustBoosters(normalizeTextList(
                    normalized.getLpAudit().getTrustBoosters(),
                    fallbackAudit.getTrustBoosters(),
                    3));
        }

        normalized.setSuggestedRules(normalizeRules(normalized.getSuggestedRules(), req, res, strategy));

        if (normalized.getScalingPlan() == null) {
            normalized.setScalingPlan(buildDefaultScalingPlan(req, res, strategy));
        } else {
            ScalingPlanDto fallbackScaling = buildDefaultScalingPlan(req, res, strategy);
            normalized.getScalingPlan().setHorizontalScaling(truncateText(
                    defaultIfBlank(normalized.getScalingPlan().getHorizontalScaling(), fallbackScaling.getHorizontalScaling()), 400));
            normalized.getScalingPlan().setBudgetScaling(truncateText(
                    defaultIfBlank(normalized.getScalingPlan().getBudgetScaling(), fallbackScaling.getBudgetScaling()), 400));
            normalized.getScalingPlan().setGeoExpansion(normalizeTextList(
                    normalized.getScalingPlan().getGeoExpansion(),
                    fallbackScaling.getGeoExpansion(),
                    3));
        }

        if (normalized.getRisks() == null) {
            normalized.setRisks(buildDefaultRiskAnalysis(req, res, strategy));
        } else {
            RiskAnalysisDto fallbackRisk = buildDefaultRiskAnalysis(req, res, strategy);
            normalized.getRisks().setFatigueWarning(truncateText(
                    defaultIfBlank(normalized.getRisks().getFatigueWarning(), fallbackRisk.getFatigueWarning()), 400));
            normalized.getRisks().setPolicyRiskLevel(normalizePolicyRiskLevel(
                    defaultIfBlank(normalized.getRisks().getPolicyRiskLevel(), fallbackRisk.getPolicyRiskLevel())));
            normalized.getRisks().setModerationTriggers(normalizeTextList(
                    normalized.getRisks().getModerationTriggers(),
                    fallbackRisk.getModerationTriggers(),
                    3));
        }

        normalized.setCompetitiveContext(truncateText(defaultIfBlank(normalized.getCompetitiveContext(), "Конкурентная среда требует отдельного тестирования офферов, аудиторий и креативных углов."), 400));
        normalized.setCiiLevel(truncateText(defaultIfBlank(normalized.getCiiLevel(), buildDefaultCiiLevel(res)), 200));
        normalized.setDifferentiationAdvice(truncateText(defaultIfBlank(normalized.getDifferentiationAdvice(), "Нужен отдельный message-market fit в креативах и посадочной странице, чтобы не конкурировать только ценой."), 400));
        normalized.setPrimaryCta(truncateText(defaultIfBlank(normalized.getPrimaryCta(), "Записаться на консультацию"), 120));
        normalized.setFunnelStage(truncateText(defaultIfBlank(normalized.getFunnelStage(), "Середина воронки - прогрев и квалификация лидов"), 200));
        normalized.setConversionMechanism(truncateText(defaultIfBlank(normalized.getConversionMechanism(), "Lead Ads -> менеджер -> квалификация -> продажа"), 220));
        normalized.setContentMix(normalizeTextList(
                normalized.getContentMix(),
                List.of("40% - демонстрационный контент", "30% - социальные доказательства", "20% - экспертный контент", "10% - продажи"),
                4));
        normalized.setBestPostingTimes(normalizeTextList(
                normalized.getBestPostingTimes(),
                List.of("09:00", "13:00", "19:00 - 21:00"),
                3));
        normalized.setPostingFrequency(defaultIfBlank(normalized.getPostingFrequency(), "1 раз в день в рабочие дни, 2 раза в выходные"));

        return normalized;
    }

    private HypothesisMatrixDto normalizeMatrix(
            HypothesisMatrixDto matrix,
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        HypothesisMatrixDto fallback = buildDefaultStrategicMatrix(req, res, strategy);
        if (matrix == null) {
            return fallback;
        }

        List<String> rows = normalizeTextList(matrix.getRows(), fallback.getRows(), 4);
        List<String> cols = normalizeTextList(matrix.getCols(), fallback.getCols(), 4);
        List<HypothesisCellDto> fallbackCells = fallback.getMatrix();
        List<HypothesisCellDto> incoming = matrix.getMatrix() != null ? matrix.getMatrix() : List.of();
        List<HypothesisCellDto> normalizedCells = new ArrayList<>();

        for (int row = 0; row < 4; row++) {
            for (int col = 0; col < 4; col++) {
                final int currentRow = row;
                final int currentCol = col;
                HypothesisCellDto source = incoming.stream()
                        .filter(cell -> cell != null && cell.getHookIndex() == currentRow && cell.getVisualIndex() == currentCol)
                        .findFirst()
                        .orElse(fallbackCells.get(row * 4 + col));

                normalizedCells.add(HypothesisCellDto.builder()
                        .hookIndex(row)
                        .visualIndex(col)
                        .specificAdIdea(truncateText(defaultIfBlank(source.getSpecificAdIdea(),
                                fallbackCells.get(row * 4 + col).getSpecificAdIdea()), 240))
                        .counterHypothesis(truncateText(defaultIfBlank(source.getCounterHypothesis(),
                                fallbackCells.get(row * 4 + col).getCounterHypothesis()), 220))
                        .funnelStage(normalizeFunnelStage(defaultIfBlank(source.getFunnelStage(),
                                fallbackCells.get(row * 4 + col).getFunnelStage())))
                        .expectedOutcome(truncateText(defaultIfBlank(source.getExpectedOutcome(),
                                fallbackCells.get(row * 4 + col).getExpectedOutcome()), 220))
                        .build());
            }
        }

        return HypothesisMatrixDto.builder()
                .rowCount(4)
                .colCount(4)
                .rows(rows)
                .cols(cols)
                .matrix(normalizedCells)
                .build();
    }

    private List<TargetingOrchestrationDto.AutomationRuleDto> normalizeRules(
            List<TargetingOrchestrationDto.AutomationRuleDto> rules,
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        List<TargetingOrchestrationDto.AutomationRuleDto> fallback = buildDefaultAutomationRules(req, res, strategy);
        if (rules == null || rules.isEmpty()) {
            return fallback;
        }
        List<TargetingOrchestrationDto.AutomationRuleDto> normalized = new ArrayList<>();
        for (TargetingOrchestrationDto.AutomationRuleDto rule : rules) {
            if (rule == null || rule.getConditions() == null || rule.getConditions().isEmpty()) {
                continue;
            }
            normalized.add(TargetingOrchestrationDto.AutomationRuleDto.builder()
                    .ruleId(defaultIfBlank(rule.getRuleId(), "rule-" + (normalized.size() + 1)))
                    .entityLevel(defaultIfBlank(rule.getEntityLevel(), "ADSET").toUpperCase(Locale.ROOT))
                    .conditionGroup(rule.getConditionGroup() != null ? rule.getConditionGroup() : TargetingOrchestrationDto.ConditionGroup.AND)
                    .conditions(rule.getConditions().stream()
                            .filter(Objects::nonNull)
                            .map(condition -> TargetingOrchestrationDto.ConditionDto.builder()
                                    .metric(defaultIfBlank(condition.getMetric(), "spend").toLowerCase(Locale.ROOT))
                                    .operator(condition.getOperator() != null ? condition.getOperator() : TargetingOrchestrationDto.Operator.GREATER_THAN)
                                    .value(condition.getValue() != null ? condition.getValue() : BigDecimal.ZERO)
                                    .build())
                            .collect(Collectors.toList()))
                    .action(defaultIfBlank(rule.getAction(), "PAUSE").toUpperCase(Locale.ROOT))
                    .actionValue(rule.getActionValue() != null ? rule.getActionValue() : BigDecimal.ZERO)
                    .build());
            if (normalized.size() == 3) {
                break;
            }
        }
        while (normalized.size() < 3) {
            normalized.add(fallback.get(normalized.size()));
        }
        return normalized;
    }

    private TargetingRecommendationDto buildFallbackRecommendation(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        TargetingRecommendationDto recommendation = buildFallbackRecommendation(req, res);
        return validateAndNormalizeRecommendation(recommendation, req, res, strategy);
    }

    private PixelCheckDto buildDefaultPixelHealth(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        boolean capi = req != null && req.getAverageCheck() != null && "HIGH".equalsIgnoreCase(req.getAverageCheck().name());
        boolean gtm = req != null && req.getUserSocialLinks() != null && !req.getUserSocialLinks().isEmpty();
        return PixelCheckDto.builder()
                .fbp(true)
                .capi(capi)
                .gtm(gtm)
                .setupInstructions("Настройте Pixel события PageView, ViewContent и Lead, передайте fbp/fbc, подключите GTM для контроля событий и включите CAPI там, где важна стабильная атрибуция.")
                .build();
    }

    private FunnelStrategyDto buildDefaultFunnelStrategy(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        String niche = req != null ? safeStr(req.getBusinessNiche()) : "продукта";
        return FunnelStrategyDto.builder()
                .tof("На холодную аудиторию показываем понятную ценность " + niche + ", демонстрацию результата и мягкий CTA без давления на покупку.")
                .mof("Для Retargeting по 50%+ видео, вовлеченным и посетителям лендинга усиливаем кейсы, FAQ, social proof и снятие возражений.")
                .bof("Горячим лидам даем hard-sell оффер: дедлайн, бонус, ограничение по местам и быстрый путь до менеджера или формы.")
                .build();
    }

    private HypothesisMatrixDto buildDefaultStrategicMatrix(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        StrategyModel model = resolveStrategyModel(strategy);
        List<String> rows = switch (model) {
            case AUTHORITY -> List.of("Экспертный разбор проблемы", "Кейсы с цифрами и исходом", "Методология и процесс работы", "Результат и измеримый эффект");
            case TRUST -> List.of("Отзывы и подтверждения", "Гарантии и прозрачность", "Реальный процесс без прикрас", "Доказательства качества");
            case CONVERSION -> List.of("Дедлайн и ограничение", "Выгода и конкретный оффер", "Прямой призыв к действию", "Сравнение потерь и выгоды");
            case ENTRY -> List.of("Простой первый шаг", "Доступность и понятный старт", "Быстрый результат без сложности", "Минимальный барьер входа");
        };
        List<String> cols = List.of("UGC-style видео", "Карточка с оффером", "Скриншот или proof-format", "До/после или демонстрация");
        List<String> competitorStrengths = extractTopCompetitorStrengths(res);
        List<HypothesisCellDto> cells = new ArrayList<>();
        for (int row = 0; row < rows.size(); row++) {
            for (int col = 0; col < cols.size(); col++) {
                String stage = row < 2 ? "TOF" : row == 2 ? "MOF" : "BOF";
                String counter = competitorStrengths.isEmpty()
                        ? "Контр-гипотеза: усиливаем дифференциацию оффера и message-market fit."
                        : "Контр-гипотеза: переиграть сильную сторону конкурента \"" + competitorStrengths.get((row + col) % competitorStrengths.size()) + "\" через более конкретный value proposition.";
                cells.add(HypothesisCellDto.builder()
                        .hookIndex(row)
                        .visualIndex(col)
                        .specificAdIdea(rows.get(row) + " + " + cols.get(col) + ": показать реальный сценарий использования, оффер и следующий шаг до заявки.")
                        .counterHypothesis(counter)
                        .funnelStage(stage)
                        .expectedOutcome("Проверить CTR, вовлечение и влияние на CPL на стадии " + stage + ".")
                        .build());
            }
        }
        return HypothesisMatrixDto.builder()
                .rowCount(4)
                .colCount(4)
                .rows(rows)
                .cols(cols)
                .matrix(cells)
                .build();
    }

    private LandingPageAuditDto buildDefaultLandingPageAudit(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        String niche = req != null ? safeStr(req.getBusinessNiche()) : "продукта";
        return LandingPageAuditDto.builder()
                .matchScore(72)
                .executiveSummary("Лендинг для " + niche + " должен быстрее раскрывать оффер, social proof и путь до конверсии. Сейчас критична ясность первого экрана и связка с рекламным сообщением.")
                .criticalFixes(List.of(
                        "Упростить первый экран: один оффер, один CTA, без перегруза текстом.",
                        "Добавить кейсы, отзывы или доказательства результата выше первого сильного скролла.",
                        "Сократить путь до заявки: меньше полей, понятная форма или быстрый мессенджер-канал."
                ))
                .trustBoosters(List.of(
                        "Показать реальные отзывы, кейсы или цифры результата с конкретикой.",
                        "Добавить прозрачные условия: сроки, стоимость, что входит в оффер.",
                        "Подсветить экспертность команды, реальные лица и подтверждения качества."
                ))
                .build();
    }

    private List<TargetingOrchestrationDto.AutomationRuleDto> buildDefaultAutomationRules(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        return List.of(
                TargetingOrchestrationDto.AutomationRuleDto.builder()
                        .ruleId("rule-kill-zero-leads")
                        .entityLevel("ADSET")
                        .conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
                        .conditions(List.of(
                                buildCondition("spend", TargetingOrchestrationDto.Operator.GREATER_THAN, 6000),
                                buildCondition("leads", TargetingOrchestrationDto.Operator.EQUAL, 0)
                        ))
                        .action("PAUSE")
                        .actionValue(BigDecimal.ZERO)
                        .build(),
                TargetingOrchestrationDto.AutomationRuleDto.builder()
                        .ruleId("rule-scale-winning-adset")
                        .entityLevel("ADSET")
                        .conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
                        .conditions(List.of(
                                buildCondition("roas", TargetingOrchestrationDto.Operator.GREATER_THAN, 4),
                                buildCondition("purchases", TargetingOrchestrationDto.Operator.GREATER_THAN_OR_EQUAL, 3)
                        ))
                        .action("INCREASE_BUDGET")
                        .actionValue(BigDecimal.valueOf(20))
                        .build(),
                TargetingOrchestrationDto.AutomationRuleDto.builder()
                        .ruleId("rule-stop-fatigue")
                        .entityLevel("AD")
                        .conditionGroup(TargetingOrchestrationDto.ConditionGroup.AND)
                        .conditions(List.of(
                                buildCondition("ctr", TargetingOrchestrationDto.Operator.LESS_THAN, 1),
                                buildCondition("frequency", TargetingOrchestrationDto.Operator.GREATER_THAN, 3)
                        ))
                        .action("PAUSE")
                        .actionValue(BigDecimal.ZERO)
                        .build()
        );
    }

    private ScalingPlanDto buildDefaultScalingPlan(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        List<String> cities = req != null && req.getPromotionCities() != null && !req.getPromotionCities().isEmpty()
                ? normalizeTextList(req.getPromotionCities(), List.of("Алматы", "Астана", "Шымкент"), 3)
                : List.of("Алматы", "Астана", "Шымкент");
        return ScalingPlanDto.builder()
                .horizontalScaling("Масштабируйте через новые lookalike/interest-кластеры, отдельные creative angles и раздельные placements для Reels, Stories и Feed.")
                .budgetScaling("Повышайте бюджет ступенчато на 15-20% каждые 48-72 часа только на связках с устойчивым CPL/ROAS.")
                .geoExpansion(cities)
                .build();
    }

    private RiskAnalysisDto buildDefaultRiskAnalysis(
            MarketingAnalysisV3Request req,
            MarketingAnalysisV3Result res,
            MarketingStrategy strategy) {
        int cii = res != null && res.getCompetitionIntensity() != null ? res.getCompetitionIntensity().getCiiIndex() : 50;
        String level = cii >= 70
                ? "High - высокий риск модерации и быстрого выгорания креативов."
                : cii >= 40
                ? "Medium - умеренный риск, нужен контроль claims и частоты."
                : "Low - низкий риск, но формулировки все равно нужно проверять.";
        return RiskAnalysisDto.builder()
                .fatigueWarning("При текущей конкурентности креативы могут выгорать за 7-14 дней, поэтому нужно заранее готовить новую матрицу углов и визуалов.")
                .policyRiskLevel(level)
                .moderationTriggers(List.of(
                        "Категоричные обещания результата без доказательств.",
                        "Формулировки, давящие на страх, боль или личную уязвимость пользователя.",
                        "Слишком агрессивные before/after claims, медицинские или финансовые обещания."
                ))
                .build();
    }

    private TargetingOrchestrationDto.ConditionDto buildCondition(
            String metric,
            TargetingOrchestrationDto.Operator operator,
            double value) {
        return TargetingOrchestrationDto.ConditionDto.builder()
                .metric(metric)
                .operator(operator)
                .value(BigDecimal.valueOf(value))
                .build();
    }

    private StrategyModel resolveStrategyModel(MarketingStrategy strategy) {
        if (strategy == null || strategy.getScoringModelName() == null || strategy.getScoringModelName().isBlank()) {
            return StrategyModel.ENTRY;
        }
        try {
            return StrategyModel.valueOf(strategy.getScoringModelName().trim().toUpperCase(Locale.ROOT));
        } catch (IllegalArgumentException ex) {
            return StrategyModel.ENTRY;
        }
    }

    private String buildDefaultCiiLevel(MarketingAnalysisV3Result res) {
        if (res == null || res.getCompetitionIntensity() == null) {
            return "Средний уровень конкуренции - нужен контролируемый тест офферов и аудиторий.";
        }
        MarketingAnalysisV3Result.CompetitionIntensity ci = res.getCompetitionIntensity();
        return safeStr(ci.getIntensityLabel()) + " (CII=" + ci.getCiiIndex() + "/100)";
    }

    private List<String> normalizeTextList(List<String> source, List<String> fallback, int minSize) {
        LinkedHashSet<String> values = new LinkedHashSet<>();
        if (source != null) {
            source.stream()
                    .filter(Objects::nonNull)
                    .map(String::trim)
                    .filter(s -> !s.isBlank())
                    .map(value -> truncateText(value, 220))
                    .forEach(values::add);
        }
        if (fallback != null) {
            fallback.stream()
                    .filter(Objects::nonNull)
                    .map(String::trim)
                    .filter(s -> !s.isBlank())
                    .forEach(values::add);
        }
        List<String> normalized = new ArrayList<>(values);
        while (normalized.size() < minSize && fallback != null && !fallback.isEmpty()) {
            normalized.add(fallback.get(normalized.size() % fallback.size()));
        }
        if (normalized.size() > minSize) {
            return normalized.subList(0, minSize);
        }
        return normalized;
    }

    private String normalizePolicyRiskLevel(String value) {
        String normalized = defaultIfBlank(value, "Medium - умеренный риск модерации.");
        String lower = normalized.toLowerCase(Locale.ROOT);
        if (lower.contains("high") || lower.contains("выс")) {
            return "High - высокий риск модерации и выгорания креативов.";
        }
        if (lower.contains("low") || lower.contains("низ")) {
            return "Low - низкий риск модерации, но формулировки все равно нужно проверять.";
        }
        return "Medium - умеренный риск модерации, нужен контроль claims и pressure tactics.";
    }

    private String normalizeFunnelStage(String value) {
        String upper = defaultIfBlank(value, "TOF").toUpperCase(Locale.ROOT);
        if (upper.contains("BOF")) return "BOF";
        if (upper.contains("MOF")) return "MOF";
        return "TOF";
    }

    private String defaultIfBlank(String value, String fallback) {
        return isBlank(value) ? fallback : value.trim();
    }

    private boolean isBlank(String value) {
        return value == null || value.isBlank();
    }

    private String truncateText(String value, int maxLen) {
        if (value == null || value.length() <= maxLen) {
            return value;
        }
        return value.substring(0, maxLen - 3).trim() + "...";
    }

    // ── Публикация первого поста ──────────────────────────────────────────────

    private PublishedPostDto publishFirstPost(MarketingStrategy strategy) {
        List<MarketingStrategy.PostCalendarItem> calendar = strategy.getPostCalendar();

        if (calendar == null || calendar.isEmpty()) {
            logger.warn("postCalendar is empty for strategyId={}", strategy.getId());
            return PublishedPostDto.builder()
                    .facebookStatus("FAILED")
                    .build();
        }

        MarketingStrategy.PostCalendarItem post = calendar.get(0);
        boolean imagePosted = false;
        String facebookPostId = null;
        String facebookStatus;

        try {
            byte[] imageBytes = downloadImage(post.getImageFilename());

            if (imageBytes != null) {
                facebookPostId = facebookPostingService.postToPageWithImage(
                        post.getPostText(), post.getHashtags(), imageBytes);
                imagePosted    = true;
                facebookStatus = "PUBLISHED";
                logger.info("Facebook post published WITH image. postId={}", facebookPostId);
            } else {
                facebookPostId = facebookPostingService.postToPage(
                        post.getPostText(), post.getHashtags());
                facebookStatus = "TEXT_ONLY";
                logger.info("Facebook post published TEXT ONLY. postId={}", facebookPostId);
            }
        } catch (Exception e) {
            logger.error("Facebook posting failed: {}", e.getMessage(), e);
            facebookStatus = "FAILED";
        }

        return PublishedPostDto.builder()
                .postIndex(0)
                .platform(safeStr(post.getPlatform()))
                .contentType(safeStr(post.getContentType()))
                .theme(safeStr(post.getTheme()))
                .postText(safeStr(post.getPostText()))
                .hashtags(post.getHashtags())
                .imageUrl(safeStr(post.getImageUrl()))
                .imagePosted(imagePosted)
                .facebookPostId(facebookPostId)
                .facebookStatus(facebookStatus)
                .build();
    }

    // ── Fallback если AI не ответил ───────────────────────────────────────────

    private TargetingRecommendationDto buildFallbackRecommendation(
            MarketingAnalysisV3Request req, MarketingAnalysisV3Result res) {

        String city = req != null && req.getPromotionCities() != null
                && !req.getPromotionCities().isEmpty()
                ? req.getPromotionCities().get(0) : "Алматы";

        int cii = res != null && res.getCompetitionIntensity() != null
                ? res.getCompetitionIntensity().getCiiIndex() : 50;

        String type = cii > 70 ? "MIXED" : cii > 40 ? "BEHAVIORAL" : "COLD_INTEREST";

        return TargetingRecommendationDto.builder()
                .recommendedType(type)
                .recommendedTypeLabel("Комбинированный таргетинг")
                .typeRationale("Рекомендация сформирована на основе общих данных ниши.")
                .campaignObjective("LEADS")
                .campaignObjectiveLabel("Генерация лидов")
                .campaignObjectiveRationale("Оптимальная цель для большинства локальных бизнесов.")
                .audienceProfile(AudienceProfileDto.builder()
                        .primarySegment("Жители " + city)
                        .ageRange("25-44")
                        .gender("Женщины 60%, Мужчины 40%")
                        .interests(List.of("местные услуги", "семья", "Казахстан"))
                        .behaviors(List.of("активные пользователи мобильных устройств"))
                        .geography(List.of(city))
                        .language("Русский, Казахский")
                        .build())
                .recommendedPlatforms(List.of("Instagram", "Facebook"))
                .recommendedAdFormats(List.of("Stories", "Feed", "Reels"))
                .budgetRecommendation(TargetingBudgetDto.builder()
                        .dailyBudget("10 000 – 20 000 KZT")
                        .monthlyBudget("300 000 – 600 000 KZT")
                        .bidStrategy("Минимальная стоимость результата")
                        .estimatedDailyReach("5 000 – 20 000 человек/день")
                        .build())
                .quickWins(List.of(
                        "Запустите Stories с CTA первым — низкий CPL",
                        "Используйте видео: CTR на 30% выше чем фото"))
                .warnings(List.of("Убедитесь что менеджер оперативно обрабатывает лиды"))
                .primaryCta("Записаться →")
                .funnelStage("Верх воронки — охват и интерес")
                .build();
    }

    // ── Итоговое сообщение ────────────────────────────────────────────────────

    private String buildSummaryMessage(
            PublishedPostDto post, TargetingRecommendationDto rec) {
        String postStatus = switch (post.getFacebookStatus()) {
            case "PUBLISHED" -> "✅ Первый пост опубликован в Facebook с изображением (ID: " + post.getFacebookPostId() + ")";
            case "TEXT_ONLY"  -> "✅ Первый пост опубликован в Facebook без изображения (ID: " + post.getFacebookPostId() + ")";
            case "FAILED"     -> "⚠️ Публикация в Facebook не удалась — проверьте токен";
            default           -> "Статус публикации неизвестен";
        };
        return postStatus + ". Тип таргетинга: " + rec.getRecommendedTypeLabel()
                + ". Цель: " + rec.getCampaignObjectiveLabel() + ".";
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // STATELESS: React-виджет — не требует MongoDB
    // ═══════════════════════════════════════════════════════════════════════════

    public MarketingAnalysisDto generateAnalysis(String topic) {
        logger.info("Stateless analysis for topic: {}", topic);

        String systemPrompt = """
                Ты — профессиональный маркетинговый аналитик с 15-летним опытом на рынках СНГ.
                Возвращай ТОЛЬКО валидный JSON без markdown. Весь текст на русском языке.
                """;

        String instruction = """
                Проанализируй бизнес и верни строго этот JSON:
                {
                  "topic": "string",
                  "marketOverview": "2-3 предложения о рынке",
                  "targetAudience": {
                    "primary": "основная аудитория",
                    "secondary": "вторичная аудитория",
                    "ageRange": "25-45",
                    "gender": "Женщины 70%, Мужчины 30%",
                    "income": "Средний и выше среднего",
                    "interests": ["интерес1","интерес2","интерес3","интерес4","интерес5"],
                    "painPoints": ["боль1","боль2","боль3"],
                    "motivations": ["мотивация1","мотивация2","мотивация3"]
                  },
                  "platforms": [
                    {"name":"Instagram","priority":"Высокий","reason":"почему"},
                    {"name":"Facebook","priority":"Средний","reason":"почему"},
                    {"name":"TikTok","priority":"Низкий","reason":"почему"}
                  ],
                  "bestAdFormats": ["Stories","Reels","Публикация","Карусель"],
                  "postingTimes": ["Пн-Пт 18:00-21:00","Сб-Вс 10:00-13:00"],
                  "budgetRecommendation": {
                    "minDaily": "5 000 KZT",
                    "optimalDaily": "15 000 KZT",
                    "monthly": "300 000 – 450 000 KZT"
                  },
                  "keyMessages": ["сообщение1","сообщение2","сообщение3"],
                  "competitorsInsight": "анализ конкурентов",
                  "recommendedVisuals": "описание лучших визуалов"
                }
                Бизнес:
                """;

        String raw = openAIAnalyticsService.generateWithInstructionWithModel(
                topic, instruction, "ru", analysisModel, systemPrompt, analysisMaxTokens, 120000L);
        return parseResponse(raw, MarketingAnalysisDto.class, "Marketing Analysis");
    }

    public MarketingStrategyDto generateStrategy(MarketingAnalysisDto analysis) {
        logger.info("Stateless strategy for topic: {}", analysis.getTopic());

        String systemPrompt = """
                Ты — старший маркетинговый стратег по performance-маркетингу в СНГ.
                Возвращай ТОЛЬКО валидный JSON без markdown. Весь текст на русском языке.
                """;

        String instruction = """
                Создай 30-дневную стратегию и верни строго этот JSON:
                {
                  "strategyName": "название",
                  "goal": "главная цель",
                  "duration": "30 дней",
                  "phases": [
                    {"phase":1,"name":"Осведомлённость","duration":"10 дней",
                     "objective":"цель","tactics":["т1","т2","т3"],"kpi":"KPI"},
                    {"phase":2,"name":"Вовлечённость","duration":"10 дней",
                     "objective":"цель","tactics":["т1","т2"],"kpi":"KPI"},
                    {"phase":3,"name":"Конверсия","duration":"10 дней",
                     "objective":"цель","tactics":["т1","т2"],"kpi":"KPI"}
                  ],
                  "adCreativesNeeded": [
                    {"id":1,"type":"Публикация в ленте","theme":"тема",
                     "captionIdea":"идея","imageDescription":"описание",
                     "cta":"CTA","targetSegment":"сегмент"},
                    {"id":2,"type":"Сторис","theme":"тема",
                     "captionIdea":"идея","imageDescription":"описание",
                     "cta":"CTA","targetSegment":"сегмент"},
                    {"id":3,"type":"Обложка Reels","theme":"тема",
                     "captionIdea":"идея","imageDescription":"описание",
                     "cta":"CTA","targetSegment":"сегмент"}
                  ],
                  "totalBudget": "450 000 KZT",
                  "expectedResults": {
                    "reach": "50 000 – 100 000 человек",
                    "clicks": "2 000 – 5 000",
                    "conversions": "100 – 300 лидов",
                    "cpl": "1 500 – 3 000 KZT"
                  }
                }
                Анализ:
                """;

        String raw = openAIAnalyticsService.generateWithInstructionWithModel(
                serializeToJson(analysis), instruction, "ru",
                strategyModel, systemPrompt, strategyMaxTokens, 120000L);
        return parseResponse(raw, MarketingStrategyDto.class, "Marketing Strategy");
    }

    public AdCreativeDto generateCreative(
            String captionIdea, MarketingAnalysisDto analysis, MarketingStrategyDto strategy) {

        logger.info("Stateless creative for idea: {}", captionIdea);

        String systemPrompt = """
                Ты — профессиональный копирайтер соцсетей для рынка Казахстана.
                Возвращай ТОЛЬКО валидный JSON без markdown. Весь текст на русском.
                """;

        String instruction = """
                Создай рекламный пост и верни строго этот JSON:
                {
                  "headline": "заголовок до 10 слов",
                  "caption": "текст 3-5 предложений с эмодзи",
                  "hashtags": ["#тег1","#тег2","#тег3","#тег4","#тег5","#тег6"],
                  "ctaButton": "Записаться →",
                  "targetingSettings": {
                    "audience": "описание аудитории",
                    "age": "возраст",
                    "interests": ["интерес1","интерес2","интерес3","интерес4"],
                    "placement": "Instagram Лента + Facebook Лента",
                    "budgetPerDay": "15 000 KZT",
                    "estimatedReach": "5 000 – 15 000 человек/день"
                  }
                }
                Данные:
                """;

        String context;
        try {
            context = objectMapper.createObjectNode()
                    .put("captionIdea", captionIdea)
                    .set("analysis", objectMapper.valueToTree(analysis))
                    .toString() + "\nСтратегия: " + serializeToJson(strategy);
        } catch (Exception e) {
            context = "Идея: " + captionIdea;
        }

        String raw = openAIAnalyticsService.generateWithInstructionWithModel(
                context, instruction, "ru", creativeModel, systemPrompt, creativeMaxTokens, 90000L);
        return parseResponse(raw, AdCreativeDto.class, "Ad Creative");
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // HELPERS
    // ═══════════════════════════════════════════════════════════════════════════

    private String extractJson(String raw) {
        if (raw == null || raw.isBlank())
            throw new IllegalStateException("Пустой ответ от AI");
        String t = raw.trim();
        if (t.startsWith("```")) {
            int nl  = t.indexOf('\n');
            int end = t.lastIndexOf("```");
            if (nl > 0 && end > nl) t = t.substring(nl + 1, end).trim();
        }
        int s = t.indexOf('{'), e = t.lastIndexOf('}');
        if (s == -1 || e == -1 || e <= s)
            throw new IllegalStateException("JSON не найден: " + t.substring(0, Math.min(200, t.length())));
        return t.substring(s, e + 1);
    }

    private <T> T parseResponse(String raw, Class<T> type, String ctx) {
        if (raw == null) throw new IllegalStateException("AI не вернул ответ: " + ctx);
        try {
            return objectMapper.readValue(extractJson(raw), type);
        } catch (IllegalStateException ex) {
            throw ex;
        } catch (Exception ex) {
            logger.error("Parse error [{}]: {}", ctx, raw, ex);
            throw new IllegalStateException("Ошибка парсинга " + ctx + ": " + ex.getMessage());
        }
    }

    private String serializeToJson(Object obj) {
        try { return objectMapper.writeValueAsString(obj); }
        catch (Exception e) { throw new IllegalStateException("Ошибка сериализации: " + e.getMessage()); }
    }

    private byte[] downloadImage(String filename) {
        if (filename == null || filename.isBlank()) return null;
        try {
            InputStream in = minIOService.downloadFile(filename);
            if (in == null) return null;
            byte[] b = in.readAllBytes();
            in.close();
            return b.length > 0 ? b : null;
        } catch (Exception e) {
            logger.warn("Cannot download image '{}': {}", filename, e.getMessage());
            return null;
        }
    }

    /** Возвращает .getDescription() у enum через рефлексию, либо .name() как fallback */
    private String describeEnum(Enum<?> e) {
        if (e == null) return "не указано";
        try {
            return (String) e.getClass().getMethod("getDescription").invoke(e);
        } catch (Exception ex) {
            return e.name();
        }
    }

    private String safeStr(String s) {
        return (s != null && !s.isBlank()) ? s : "";
    }

    private String formatList(List<String> list) {
        return (list == null || list.isEmpty()) ? "не указано" : String.join(", ", list);
    }
}
