package kz.konturai.parser.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import kz.konturai.parser.dto.MarketingStrategyRequest;
import kz.konturai.parser.dto.ScoringResult;
import kz.konturai.parser.dto.StatusHistoryEntry;
import kz.konturai.parser.enums.StrategyModel;
import kz.konturai.parser.enums.StrategyModifier;
import kz.konturai.parser.model.MarketingAnalysisV3Document;
import kz.konturai.parser.model.MarketingStrategy;
import kz.konturai.parser.model.PostingTask;
import kz.konturai.parser.repository.MarketingAnalysisV3Repository;
import kz.konturai.parser.repository.MarketingStrategyRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.MediaType;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class MarketingStrategyV3Service {

    private final MarketingStrategyRepository repository;
    private final MarketingAnalysisV3Repository analysisRepository;
    private final OpenAIAnalyticsService aiService;
    private final ImageGenerationService imageGenerationService;
    private final GeminiVideoGenerationService geminiVideoService;
    private final PostingTaskService postingTaskService;
    private final MinIOService minIOService;
    private final FacebookPostingService facebookPostingService;

    @Autowired
    @Lazy
    private MarketingStrategyV3Service self;

    private final ObjectMapper objectMapper = new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

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

    @Value("${image.generation.delayBetweenRequestsMs:3000}")
    private long delayBetweenRequestsMs;

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

    public Optional<MarketingStrategy> getStrategyById(String id) {
        return repository.findById(id);
    }

    public Optional<MarketingStrategy> getStrategyByAnalysisId(String analysisId) {
        return repository.findFirstByAnalysisIdOrderByCreatedAtDesc(analysisId);
    }

    public List<MarketingStrategy> getUserStrategies(String userId) {
        return repository.findByUserIdOrderByCreatedAtDesc(userId);
    }

    public MarketingStrategy generateStrategy(String analysisId, MarketingStrategyRequest request,
            String userId, List<String> referenceFilenames) {
        MarketingAnalysisV3Document analysisDoc = analysisRepository.findById(analysisId)
                .orElseThrow(() -> new IllegalArgumentException("Analysis V3 not found: " + analysisId));

        MarketingStrategy strategy = new MarketingStrategy(analysisId);
        strategy.setUserId(userId);
        strategy.setDurationWeeks(4);
        strategy.setPriorityPlatforms(new ArrayList<>());
        strategy.setStatus("queued");

        Map<String, Object> initialData = new HashMap<>();
        if (referenceFilenames != null && !referenceFilenames.isEmpty()) {
            initialData.put("referenceFilenames", referenceFilenames);
        }
        strategy.setStrategyData(initialData);

        addStatusHistory(strategy, "queued",
                "Запуск генерации. Система рассчитывает оптимальную стратегическую модель...");
        strategy = repository.save(strategy);

        self.processStrategyGenerationAsync(strategy.getId(), analysisDoc, strategy);
        return strategy;
    }

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

    @Async("reportGenerationExecutor")
    public void processStrategyGenerationAsync(String strategyId,
            MarketingAnalysisV3Document analysis,
            MarketingStrategy strategy) {
        try {
            strategy.setStatus("processing");
            addStatusHistory(strategy, "processing", "Запускаю скоринговую систему...");
            repository.save(strategy);

            // Step 1: Scoring
            ScoringResult scoring = calculateScoringModel(analysis.getRequestData(), analysis);

            log.info("[Strategy {}] Scoring → model={} (E:{} Au:{} T:{} C:{}) modifiers={}",
                    strategyId, scoring.getModel(),
                    scoring.getEntryScore(), scoring.getAuthorityScore(),
                    scoring.getTrustScore(), scoring.getConversionScore(),
                    scoring.getActiveModifiers().stream()
                            .map(StrategyModifier::getTitle).collect(Collectors.joining(", ")));

            strategy.setScoringModelName(scoring.getModel().name());
            strategy.setScoringModelTitle(scoring.getModel().getTitle());
            addStatusHistory(strategy, "processing", "✅ " + scoring.getRationale());
            repository.save(strategy);

            // Step 2: Build prompts
            addStatusHistory(strategy, "processing", "Генерирую контент-план на 4 недели...");
            repository.save(strategy);

            String systemPrompt = buildSystemPrompt(scoring, analysis);
            String userPrompt = buildUserPrompt(analysis, scoring);

            // Step 3: AI call
            String rawResponse = aiService.generateWithInstructionWithModel(
                    "{}", userPrompt, "ru", highIntelligenceModel,
                    systemPrompt, 16000, 240_000L);

            String jsonResponse = extractCleanJson(rawResponse);
            if (jsonResponse == null)
                throw new IllegalStateException("AI вернул невалидный JSON");

            Map<String, Object> strategyData = objectMapper.readValue(jsonResponse, new TypeReference<>() {
            });

            if (strategy.getStrategyData() != null) {
                strategy.getStrategyData().forEach(strategyData::putIfAbsent);
            }

            strategyData.put("scoringModel", scoring.getModel().name());
            strategyData.put("scoringModelTitle", scoring.getModel().getTitle());
            strategyData.put("scoringRationale", scoring.getRationale());
            strategyData.put("activeModifiers",
                    scoring.getActiveModifiers().stream()
                            .map(StrategyModifier::getTitle).collect(Collectors.toList()));
            strategyData.put("scores", Map.of(
                    "entry", scoring.getEntryScore(),
                    "authority", scoring.getAuthorityScore(),
                    "trust", scoring.getTrustScore(),
                    "conversion", scoring.getConversionScore()));

            populateStrategyEntity(strategy, strategyData, scoring);
            repository.save(strategy);

            // Step 4: Чистим хэштеги от случайных двойных ##
            cleanHashtags(strategy);
            repository.save(strategy);

            // Step 5: Media generation
            addStatusHistory(strategy, "processing",
                    "Создаю фото и видео под каждый пост. Это займёт несколько минут...");
            repository.save(strategy);

            generateMediaAssets(strategy, analysis);

            strategy.setStatus("completed");
            strategy.setCompletedAt(LocalDateTime.now());
            addStatusHistory(strategy, "completed",
                    "🎉 Стратегия готова! Модель: " + scoring.getModel().getTitle());
            repository.save(strategy);

            log.info("[Strategy {}] Completed. Model={}, Posts={}",
                    strategyId, scoring.getModel(),
                    strategy.getPostCalendar() != null ? strategy.getPostCalendar().size() : 0);

        } catch (Exception e) {
            log.error("[Strategy {}] FAILED: {}", strategyId, e.getMessage(), e);
            markAsFailed(strategyId, "Ошибка при генерации стратегии: " + e.getMessage());
        }
    }

    // =====================================================================
    // SCORING ENGINE
    // =====================================================================

    public ScoringResult calculateScoringModel(
            kz.konturai.parser.dto.MarketingAnalysisV3Request req,
            MarketingAnalysisV3Document analysis) {

        int entry = 0, authority = 0, trust = 0, conversion = 0;
        List<String> rationaleFactors = new ArrayList<>();

        if (req.getCustomerBehaviors() != null) {
            for (var cb : req.getCustomerBehaviors()) {
                switch (cb) {
                    case BUY_QUICKLY -> {
                        conversion += 3;
                        rationaleFactors.add("клиенты покупают быстро");
                    }
                    case COMPARE_OPTIONS -> {
                        authority += 2;
                        trust += 1;
                        rationaleFactors.add("клиенты сравнивают варианты");
                    }
                    case REQUEST_PROPOSAL -> {
                        authority += 3;
                        rationaleFactors.add("клиенты просят расчёт/КП");
                    }
                    case CHECK_REVIEWS -> {
                        trust += 3;
                        rationaleFactors.add("клиенты проверяют кейсы и отзывы");
                    }
                    case NEED_CONSULTATION -> {
                        authority += 3;
                        rationaleFactors.add("клиенты приходят на консультацию");
                    }
                }
            }
        }
        if (req.getAverageCheck() != null) {
            switch (req.getAverageCheck()) {
                case LOW -> {
                    conversion += 2;
                    rationaleFactors.add("низкий средний чек");
                }
                case MEDIUM -> {
                    authority += 1;
                    trust += 1;
                    conversion += 1;
                    rationaleFactors.add("средний чек");
                }
                case HIGH -> {
                    authority += 2;
                    trust += 1;
                    rationaleFactors.add("высокий средний чек");
                }
            }
        }
        if (req.getBusinessStage() != null) {
            switch (req.getBusinessStage()) {
                case JUST_STARTING -> {
                    entry += 3;
                    rationaleFactors.add("бизнес только запускается");
                }
                case LESS_THAN_ONE_YEAR -> {
                    entry += 2;
                    authority += 1;
                    rationaleFactors.add("менее года");
                }
                case ONE_TO_THREE_YEARS -> {
                    authority += 1;
                    trust += 1;
                    conversion += 1;
                    rationaleFactors.add("1–3 года");
                }
                case MORE_THAN_THREE_YEARS -> {
                    authority += 1;
                    trust += 1;
                    conversion += 1;
                    rationaleFactors.add("более 3 лет");
                }
            }
        }
        if (req.getClientTarget() != null) {
            switch (req.getClientTarget()) {
                case BUSINESS -> {
                    authority += 2;
                    trust += 1;
                    rationaleFactors.add("аудитория B2B");
                }
                case PRIVATE_CLIENTS -> {
                    trust += 1;
                    conversion += 2;
                    rationaleFactors.add("аудитория B2C");
                }
                case MIXED -> {
                    authority += 1;
                    trust += 1;
                    conversion += 1;
                    rationaleFactors.add("B2B+B2C");
                }
            }
        }
        if (analysis != null && analysis.getResultData() != null
                && analysis.getResultData().getCompetitionIntensity() != null) {
            int cii = analysis.getResultData().getCompetitionIntensity().getCiiIndex();
            if (cii < 35) {
                conversion += 1;
                rationaleFactors.add("низкая конкуренция (CII=" + cii + ")");
            } else if (cii < 65) {
                trust += 1;
                conversion += 1;
                rationaleFactors.add("средняя конкуренция (CII=" + cii + ")");
            } else {
                trust += 2;
                rationaleFactors.add("высокая конкуренция (CII=" + cii + ")");
            }
        }

        int maxScore = Math.max(Math.max(entry, authority), Math.max(trust, conversion));
        StrategyModel selectedModel = resolveModelWithTiebreaker(
                entry, authority, trust, conversion, maxScore, req, analysis);
        List<StrategyModifier> modifiers = resolveModifiers(req, analysis);
        String rationale = buildRationaleText(selectedModel, rationaleFactors,
                entry, authority, trust, conversion);

        return new ScoringResult(selectedModel, entry, authority, trust, conversion, modifiers, rationale);
    }

    private StrategyModel resolveModelWithTiebreaker(
            int entry, int authority, int trust, int conversion, int maxScore,
            kz.konturai.parser.dto.MarketingAnalysisV3Request req,
            MarketingAnalysisV3Document analysis) {

        boolean highCheck = req.getAverageCheck() != null
                && req.getAverageCheck() == kz.konturai.parser.enums.AverageCheck.HIGH;
        boolean highComp = analysis != null && analysis.getResultData() != null
                && analysis.getResultData().getCompetitionIntensity() != null
                && analysis.getResultData().getCompetitionIntensity().getCiiIndex() >= 65;
        boolean isLaunch = req.getBusinessStage() != null
                && req.getBusinessStage() == kz.konturai.parser.enums.BusinessStage.JUST_STARTING;

        if (authority == maxScore && trust == maxScore && highCheck)
            return StrategyModel.AUTHORITY;
        if (trust == maxScore && conversion == maxScore && highComp)
            return StrategyModel.TRUST;
        if (entry == maxScore && isLaunch)
            return StrategyModel.ENTRY;
        if (authority == maxScore)
            return StrategyModel.AUTHORITY;
        if (conversion == maxScore)
            return StrategyModel.CONVERSION;
        if (trust == maxScore)
            return StrategyModel.TRUST;
        return StrategyModel.ENTRY;
    }

    private List<StrategyModifier> resolveModifiers(
            kz.konturai.parser.dto.MarketingAnalysisV3Request req,
            MarketingAnalysisV3Document analysis) {
        List<StrategyModifier> mods = new ArrayList<>();
        if (mods.size() < 3 && analysis != null && analysis.getResultData() != null
                && analysis.getResultData().getCompetitionIntensity() != null
                && analysis.getResultData().getCompetitionIntensity().getCiiIndex() > 60)
            mods.add(StrategyModifier.HIGH_COMPETITION);
        if (mods.size() < 3 && req.getAverageCheck() != null
                && req.getAverageCheck() == kz.konturai.parser.enums.AverageCheck.HIGH)
            mods.add(StrategyModifier.HIGH_CHECK);
        if (mods.size() < 3 && req.getCustomerBehaviors() != null
                && req.getCustomerBehaviors().contains(kz.konturai.parser.enums.CustomerBehavior.BUY_QUICKLY))
            mods.add(StrategyModifier.SHORT_DECISION);
        if (mods.size() < 3 && req.getCustomerBehaviors() != null
                && req.getCustomerBehaviors().contains(kz.konturai.parser.enums.CustomerBehavior.CHECK_REVIEWS))
            mods.add(StrategyModifier.SOCIAL_PROOF_SENSITIVE);
        if (mods.size() < 3 && req.getPurchaseFrequency() != null
                && req.getPurchaseFrequency() == kz.konturai.parser.enums.PurchaseFrequency.REGULARLY)
            mods.add(StrategyModifier.RETENTION);
        if (mods.size() < 3) {
            List<String> cities = req.getPromotionCities() != null ? req.getPromotionCities()
                    : (req.getPresenceCities() != null ? req.getPresenceCities() : List.of());
            if (cities.size() > 1)
                mods.add(StrategyModifier.MULTI_GEO);
        }
        return mods;
    }

    private String buildRationaleText(StrategyModel model, List<String> factors,
            int entry, int authority, int trust, int conversion) {
        String top = factors.stream().limit(3).collect(Collectors.joining(", "));
        return String.format("Выбрана модель %s (E=%d Au=%d T=%d C=%d). Причины: %s.",
                model.getTitle(), entry, authority, trust, conversion,
                top.isEmpty() ? "анализ профиля бизнеса" : top);
    }

    // =====================================================================
    // PROMPT BUILDING
    // =====================================================================

    /**
     * Системный промпт — теперь включает контекст ниши для более релевантного
     * контента.
     */
    private String buildSystemPrompt(ScoringResult scoring, MarketingAnalysisV3Document analysis) {
        String modifiersBlock = scoring.getActiveModifiers().isEmpty()
                ? "Нет активных модификаторов."
                : scoring.getActiveModifiers().stream()
                        .map(StrategyModifier::getInstructions).collect(Collectors.joining("\n"));

        // Извлекаем конкурентов из анализа для примеров контента
        String competitorContext = "";
        if (analysis != null && analysis.getResultData() != null
                && analysis.getResultData().getCompetitorMap() != null
                && !analysis.getResultData().getCompetitorMap().isEmpty()) {
            competitorContext = analysis.getResultData().getCompetitorMap().stream()
                    .limit(3)
                    .map(c -> "  • " + c.getName() + ": сильные стороны — "
                            + (c.getStrengths() != null ? String.join(", ", c.getStrengths()) : "N/A"))
                    .collect(Collectors.joining("\n"));
        }

        String niche = (analysis != null && analysis.getRequestData() != null)
                ? analysis.getRequestData().getBusinessNiche()
                : "бизнес";
        String product = (analysis != null && analysis.getRequestData() != null)
                ? analysis.getRequestData().getProductName()
                : "";

        return String.format("""
                ТЫ — СТАРШИЙ SMM СТРАТЕГ РЫНКА КАЗАХСТАНА С 15-ЛЕТНИМ ОПЫТОМ.
                Твоя задача — создать контент-план, который РЕАЛЬНО ПРИНОСИТ ЛИДЫ.

                ════════ БИЗНЕС КЛИЕНТА ════════
                Ниша: %s
                Продукт/Бренд: %s
                Выбранная стратегическая модель: %s
                Обоснование: %s

                ════════ КОНКУРЕНТЫ (учти при создании контента) ════════
                %s

                ════════ ПРАВИЛА МОДЕЛИ (СТРОГО) ════════
                %s

                ════════ МОДИФИКАТОРЫ ════════
                %s

                ════════ КРИТИЧЕСКИЕ ПРАВИЛА — НАРУШАТЬ ЗАПРЕЩЕНО ════════

                ПРАВИЛО 1 — ТЕМА ПОСТА = КОНКРЕТНАЯ СИТУАЦИЯ В НИШЕ:
                Каждый пост — это конкретная история или кейс из ниши "%s".
                ЗАПРЕЩЕНО: абстрактные темы ("О нашей компании", "Введение в продукт")
                ОБЯЗАТЕЛЬНО: конкретика ("Как выбрать ракетку для начинающего", "3 ошибки при первой тренировке")

                ПРАВИЛО 2 — ТЕКСТ ПОСТА = ПРОДАЮЩИЙ КОНТЕНТ НА РУССКОМ:
                - Минимум 150 слов живого текста
                - Первые 2 строки — КРЮЧОК, который останавливает скролл
                - Конкретные цифры, факты, примеры из казахстанской реальности
                - В конце — ОДИН конкретный призыв к действию (запись, звонок, директ)
                - Тенге, алматинские районы, казахстанские реалии

                ПРАВИЛО 3 — ХЭШТЕГИ (ТОЛЬКО ОДИНОЧНЫЙ #):
                - От 5 до 8 хэштегов на русском/казахском
                - Каждый хэштег начинается с ОДНОГО символа #
                - ЗАПРЕЩЕНО: ##хэштег, # хэштег, английские хэштеги
                - ПРАВИЛЬНО: #теннисалматы #спортказахстан #алматы

                ПРАВИЛО 4 — ФОРМАТЫ:
                - contentType: ТОЛЬКО "фото" или "видео"
                - РОВНО 2 видео-поста на весь план — расставить на самые важные посты
                - Остальные посты — "фото"

                ПРАВИЛО 5 — РАСПИСАНИЕ:
                - Instagram: 09:00, 12:00, 18:00, 20:00
                - TikTok: 18:00, 20:00, 22:00
                - Не публиковать чаще 1 раза в день на одной платформе

                ВЫВОДИ ТОЛЬКО ЧИСТЫЙ JSON! Без markdown, без пояснений.
                """,
                niche, product,
                scoring.getModel().getTitle(), scoring.getRationale(),
                competitorContext.isEmpty() ? "Данные конкурентов не загружены." : competitorContext,
                scoring.getModel().getContentRules().replace("%", "%%"),
                modifiersBlock.replace("%", "%%"),
                niche);
    }

    private String buildUserPrompt(MarketingAnalysisV3Document analysis,
            ScoringResult scoring) throws Exception {
        String clientJson = objectMapper.writeValueAsString(analysis.getRequestData());
        String analysisCtx = buildAnalysisContext(analysis);
        String postCountPlan = buildPostCountPlan(scoring.getModel(), 4);
        LocalDateTime startDate = LocalDateTime.now().plusDays(1);

        String niche = analysis.getRequestData() != null
                ? analysis.getRequestData().getBusinessNiche()
                : "бизнес";

        return String.format(
                """
                        ЗАДАНИЕ: Создай ПРОФЕССИОНАЛЬНУЮ SMM-стратегию на 4 недели для "%s" на рынке Казахстана.
                        Стратегия должна РЕАЛЬНО ПРИВЛЕКАТЬ КЛИЕНТОВ И ПРИНОСИТЬ ЛИДЫ.

                        ══ ПРОФИЛЬ БИЗНЕСА ══
                        %s

                        ══ ДАННЫЕ РЫНКА ══
                        %s

                        ══ ПАРАМЕТРЫ ══
                        Старт: %s | Модель: %s | Модификаторы: %s

                        ══ ПЛАН ПОСТОВ ══
                        %s

                        ════════════════════════════════════════════════════════════════
                        ⚠️  КРИТИЧЕСКИ ВАЖНО: РАСПРЕДЕЛЕНИЕ ПОСТОВ ПО НЕДЕЛЯМ
                        ════════════════════════════════════════════════════════════════

                        ОБЩЕЕ КОЛИЧЕСТВО: РОВНО 16 ПОСТОВ за 4 недели (4 поста в неделю).

                        РАСПРЕДЕЛЕНИЕ ПО НЕДЕЛЯМ — СТРОГО:

                        НЕДЕЛЯ 1 (дни 1-7 от старта):
                          - Пост 1: День 1 (понедельник) — время 09:00
                          - Пост 2: День 3 (среда) — время 12:00
                          - Пост 3: День 5 (пятница) — время 18:00
                          - Пост 4: День 6 (суббота) — время 20:00

                        НЕДЕЛЯ 2 (дни 8-14):
                          - Пост 5: День 8 (понедельник) — время 09:00
                          - Пост 6: День 10 (среда) — время 12:00
                          - Пост 7: День 12 (пятница) — время 18:00
                          - Пост 8: День 13 (суббота) — время 20:00

                        НЕДЕЛЯ 3 (дни 15-21):
                          - Пост 9: День 15 (понедельник) — время 09:00
                          - Пост 10: День 17 (среда) — время 12:00
                          - Пост 11: День 19 (пятница) — время 18:00
                          - Пост 12: День 20 (суббота) — время 20:00

                        НЕДЕЛЯ 4 (дни 22-28):
                          - Пост 13: День 22 (понедельник) — время 09:00
                          - Пост 14: День 24 (среда) — время 12:00
                          - Пост 15: День 26 (пятница) — время 18:00
                          - Пост 16: День 27 (суббота) — время 20:00

                        ВАЖНО:
                        - КАЖДЫЙ пост в postCalendar должен иметь поле "weekNumber" (1, 2, 3 или 4)
                        - Посты НЕ должны пересекаться между неделями
                        - Соблюдай равномерный ритм: Пн-Ср-Пт-Сб каждую неделю
                        - Никаких пропусков в 2-3 дня между постами внутри одной недели!

                        ══ JSON СТРУКТУРА ══
                        {
                          "selectedModel": "%s",
                          "scoringRationale": "%s",
                          "recommendedDurationWeeks": 4,
                          "recommendedPlatforms": ["Instagram", "TikTok"],
                          "rationale": "Минимум 60 слов — почему именно эти платформы для данной ниши",
                          "weeklyPlans": [
                            {
                              "weekNumber": 1,
                              "mainThemes": [
                                "Конкретная тема 1 для недели 1",
                                "Конкретная тема 2 для недели 1",
                                "Конкретная тема 3 для недели 1",
                                "Конкретная тема 4 для недели 1"
                              ],
                              "contentRecommendations": "Что конкретно снимать и писать на неделе 1 — минимум 50 слов",
                              "priorityPlatforms": ["Instagram"]
                            }
                          ],
                          "postCalendar": [
                            {
                              "publishDate": "2024-01-15T09:00:00",
                              "weekNumber": 1,
                              "platform": "Instagram",
                              "contentType": "фото",
                              "theme": "КОНКРЕТНАЯ тема поста — не общая фраза, а конкретная ситуация из ниши",
                              "postText": "150+ слов живого текста на русском. Первые 2 строки — крючок. Цифры и факты. CTA в конце.",
                              "hashtags": ["#хэштег1", "#хэштег2", "#алматы"],
                              "publishTime": "09:00"
                            }
                          ]
                        }

                        ЖЁСТКИЕ ТРЕБОВАНИЯ:
                        1. postCalendar: РОВНО 16 постов (4 поста × 4 недели)
                        2. РОВНО 4 поста с contentType: "видео" (по 1 видео в неделю), остальные "фото"
                        3. КАЖДЫЙ пост должен иметь "weekNumber": 1, 2, 3 или 4
                        4. Даты должны соответствовать неделе: weekNumber 1 = дни 1-7, weekNumber 2 = дни 8-14 и т.д.
                        5. theme: конкретная тема из ниши "%s" — никаких абстракций
                        6. postText: 150+ слов, только русский язык
                        7. hashtags: 5-8 штук, каждый начинается с одного #, только русский/казахский
                        8. weeklyPlans: у каждой недели должно быть РОВНО 4 темы в mainThemes

                        ПРИМЕР ПРАВИЛЬНОГО РАСПРЕДЕЛЕНИЯ:
                        {
                          "weeklyPlans": [
                            {
                              "weekNumber": 1,
                              "mainThemes": [
                                "Кейс: Как мы увеличили продажи клиента на 40%% за месяц",
                                "Процесс: За кадром нашей работы",
                                "Отзыв: История довольного клиента из Алматы",
                                "Экспертный: 5 ошибок при выборе услуги"
                              ]
                            }
                          ],
                          "postCalendar": [
                            {
                              "publishDate": "2026-04-13T09:00:00",
                              "weekNumber": 1,
                              "platform": "Instagram",
                              "contentType": "видео",
                              "theme": "Кейс: Как мы увеличили продажи клиента на 40%% за месяц"
                            },
                            {
                              "publishDate": "2026-04-15T12:00:00",
                              "weekNumber": 1,
                              "platform": "Instagram",
                              "contentType": "фото",
                              "theme": "Процесс: За кадром нашей работы"
                            }
                          ]
                        }

                        ОТВЕТ: только JSON объект. Никакого текста вне JSON.
                        """,
                niche, clientJson, analysisCtx,
                startDate.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
                scoring.getModel().getTitle(),
                scoring.getActiveModifiers().stream()
                        .map(StrategyModifier::getTitle).collect(Collectors.joining(", ")),
                postCountPlan,
                scoring.getModel().name(),
                scoring.getRationale().replace("\"", "'"),
                niche);
    }

    private String buildAnalysisContext(MarketingAnalysisV3Document analysis) {
        if (analysis == null || analysis.getResultData() == null)
            return "Данные анализа недоступны.";
        var r = analysis.getResultData();
        var sb = new StringBuilder();
        if (r.getExecutiveSummary() != null) {
            var es = r.getExecutiveSummary();
            sb.append("Конкурентов: ").append(es.getActiveCompetitors())
                    .append(" | Конкуренция: ").append(es.getCompetitionLevel())
                    .append(" | ER ниши: ").append(es.getAverageNicheEr()).append("%")
                    .append(" | Рейтинг: ").append(es.getAverageRating()).append("/5\n");
            if (es.getKeyFigures() != null && !es.getKeyFigures().isEmpty())
                sb.append("Факты: ").append(String.join("; ", es.getKeyFigures())).append("\n");
        }
        if (r.getCompetitionIntensity() != null)
            sb.append("CII: ").append(r.getCompetitionIntensity().getCiiIndex())
                    .append("/100 — ").append(r.getCompetitionIntensity().getIntensityLabel()).append("\n");
        if (r.getContentProfile() != null)
            sb.append("Видео у конкурентов: ").append(r.getContentProfile().getVideoShare())
                    .append("% | CTA используют: ").append(r.getContentProfile().getCtaUsagePercent()).append("%\n");
        if (r.getBehavioralPattern() != null && r.getBehavioralPattern().getCommonCta() != null)
            sb.append("Топ CTA конкурентов: ")
                    .append(String.join(", ", r.getBehavioralPattern().getCommonCta())).append("\n");
        if (r.getSearchDemand() != null) {
            sb.append("Спрос: ").append(r.getSearchDemand().getAvgMonthlySearches()).append(" запросов/мес\n");
            if (r.getSearchDemand().getRelatedQueries() != null && !r.getSearchDemand().getRelatedQueries().isEmpty())
                sb.append("Популярные запросы: ")
                        .append(String.join(", ", r.getSearchDemand().getRelatedQueries())).append("\n");
        }
        if (r.getStructuredConclusions() != null && !r.getStructuredConclusions().isEmpty()) {
            sb.append("Выводы анализа:\n");
            r.getStructuredConclusions().forEach(c -> sb.append("  • ").append(c).append("\n"));
        }
        return sb.toString();
    }

    private String buildPostCountPlan(StrategyModel model, int weeks) {
        int total = weeks * 4; // 4 поста в неделю × 4 недели = 16 постов
        StringBuilder sb = new StringBuilder();
        sb.append("Общее количество: ").append(total).append(" постов (4 поста/неделю × ").append(weeks)
                .append(" недели)\n");
        sb.append("РОВНО 4 видео-поста (по 1 в неделю), остальные фото\n\n");
        sb.append("РАСПРЕДЕЛЕНИЕ ПО НЕДЕЛЯМ:\n");
        sb.append("  Неделя 1: 4 поста (Пн 09:00, Ср 12:00, Пт 18:00, Сб 20:00)\n");
        sb.append("  Неделя 2: 4 поста (Пн 09:00, Ср 12:00, Пт 18:00, Сб 20:00)\n");
        sb.append("  Неделя 3: 4 поста (Пн 09:00, Ср 12:00, Пт 18:00, Сб 20:00)\n");
        sb.append("  Неделя 4: 4 поста (Пн 09:00, Ср 12:00, Пт 18:00, Сб 20:00)\n\n");
        sb.append("Типы постов:\n");
        switch (model) {
            case ENTRY -> {
                sb.append("  Объясняющий: ").append(pct(total, 0.30)).append(" (30%)\n");
                sb.append("  Демонстрационный: ").append(pct(total, 0.25)).append(" (25%)\n");
                sb.append("  Социальное доказательство: ").append(pct(total, 0.20)).append(" (20%)\n");
                sb.append("  Экспертный: ").append(pct(total, 0.15)).append(" (15%)\n");
                sb.append("  Продающий: ").append(pct(total, 0.10)).append(" (10%)\n");
            }
            case AUTHORITY -> {
                sb.append("  Экспертный: ").append(pct(total, 0.40)).append(" (40%)\n");
                sb.append("  Кейсы: ").append(pct(total, 0.25)).append(" (25%)\n");
                sb.append("  Объясняющий: ").append(pct(total, 0.15)).append(" (15%)\n");
                sb.append("  Социальное доказательство: ").append(pct(total, 0.10)).append(" (10%)\n");
                sb.append("  Продающий: ").append(pct(total, 0.10)).append(" (10%)\n");
            }
            case TRUST -> {
                sb.append("  Кейсы: ").append(pct(total, 0.30)).append(" (30%)\n");
                sb.append("  Отзывы: ").append(pct(total, 0.25)).append(" (25%)\n");
                sb.append("  Процесс: ").append(pct(total, 0.20)).append(" (20%)\n");
                sb.append("  Экспертный: ").append(pct(total, 0.15)).append(" (15%)\n");
                sb.append("  Продающий: ").append(pct(total, 0.10)).append(" (10%)\n");
            }
            case CONVERSION -> {
                sb.append("  Продающий: ").append(pct(total, 0.35)).append(" (35%)\n");
                sb.append("  Демонстрационный: ").append(pct(total, 0.30)).append(" (30%)\n");
                sb.append("  Вовлекающий: ").append(pct(total, 0.20)).append(" (20%)\n");
                sb.append("  Социальное доказательство: ").append(pct(total, 0.10)).append(" (10%)\n");
                sb.append("  Объясняющий: ").append(pct(total, 0.05)).append(" (5%)\n");
            }
        }
        return sb.toString();
    }

    private int pct(int total, double ratio) {
        return (int) Math.round(total * ratio);
    }

    // =====================================================================
    // HASHTAG CLEANUP — чистим ## и лишние пробелы
    // =====================================================================

    private void cleanHashtags(MarketingStrategy strategy) {
        if (strategy.getPostCalendar() == null)
            return;
        for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) {
            if (item.getHashtags() == null)
                continue;
            List<String> cleaned = item.getHashtags().stream()
                    .map(tag -> {
                        // Убираем пробелы
                        tag = tag.trim();
                        // Убираем все # в начале
                        while (tag.startsWith("#"))
                            tag = tag.substring(1);
                        // Убираем пробелы внутри тега
                        tag = tag.replaceAll("\\s+", "");
                        // Добавляем ровно один #
                        return tag.isEmpty() ? null : "#" + tag;
                    })
                    .filter(Objects::nonNull)
                    .distinct()
                    .collect(Collectors.toList());
            item.setHashtags(cleaned);
        }
    }

    // =====================================================================
    // MEDIA GENERATION
    // =====================================================================

    @SuppressWarnings("unchecked")
    private void generateMediaAssets(MarketingStrategy strategy,
            MarketingAnalysisV3Document analysis) {
        String niche = safeGet(analysis, a -> a.getRequestData().getBusinessNiche(), "бизнес");
        String brand = safeGet(analysis, a -> a.getRequestData().getProductName(), "");
        String mainCity = safeGet(analysis, a -> {
            String c = a.getRequestData().getMainCity();
            return (c != null && !c.isBlank()) ? c : "Алматы";
        }, "Алматы");

        List<String> refFiles = new ArrayList<>();
        if (strategy.getStrategyData() != null
                && strategy.getStrategyData().containsKey("referenceFilenames")) {
            refFiles = (List<String>) strategy.getStrategyData().get("referenceFilenames");
        }

        Random random = new Random();
        int total = strategy.getPostCalendar().size();
        int current = 0;

        // Считаем сколько видео уже запланировано AI
        long plannedVideos = strategy.getPostCalendar().stream()
                .filter(p -> "видео".equalsIgnoreCase(p.getContentType()))
                .count();
        log.info("[Media] Total posts={}, planned videos={}", total, plannedVideos);

        // Если AI не назначил ни одного видео — принудительно назначаем 4 (по 1 в
        // неделю)
        if (plannedVideos == 0) {
            log.warn("[Media] AI не назначил ни одного видео! Принудительно назначаю 4 поста как видео.");
            int assigned = 0;
            for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) {
                if (assigned >= 4)
                    break;
                // Назначаем видео на первый пост каждой недели
                if (item.getWeekNumber() != null && (item.getWeekNumber() - 1) * 4 <= assigned
                        && assigned < (item.getWeekNumber()) * 4) {
                    item.setContentType("видео");
                    assigned++;
                }
            }
            // Если не получилось назначить по неделе, просто назначаем первые 4
            if (assigned < 4) {
                for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) {
                    if (assigned >= 4)
                        break;
                    if (!"видео".equals(item.getContentType())) {
                        item.setContentType("видео");
                        assigned++;
                    }
                }
            }
        }

        int videoGenerated = 0;

        for (MarketingStrategy.PostCalendarItem item : strategy.getPostCalendar()) {
            current++;
            String contentType = item.getContentType() != null
                    ? item.getContentType().trim().toLowerCase()
                    : "фото";

            // Нормализуем — на случай если AI написал что-то кроме "фото"/"видео"
            if (!contentType.equals("видео"))
                contentType = "фото";

            log.info("[Media {}/{}] type='{}' platform='{}' theme='{}'",
                    current, total, contentType,
                    item.getPlatform(), item.getTheme());

            // Лимит: максимум 4 видео (по 1 в неделю)
            if (contentType.equals("видео") && videoGenerated >= 4) {
                log.warn("[Media] Video limit (4) reached — switching post {} to photo", current);
                contentType = "фото";
                item.setContentType("фото");
            }

            // Загружаем референсный файл если есть
            byte[] refBytes = null;
            if (!refFiles.isEmpty()) {
                String selectedRef = refFiles.get(random.nextInt(refFiles.size()));
                try (InputStream s = minIOService.downloadFile(selectedRef)) {
                    refBytes = s.readAllBytes();
                } catch (Exception e) {
                    log.warn("[Media] Failed to load ref file {}: {}", selectedRef, e.getMessage());
                }
            }

            try {
                if (contentType.equals("видео")) {
                    boolean success = doGenerateVideo(item, niche, brand, mainCity);
                    if (success) {
                        videoGenerated++;
                    } else {
                        // Видео не получилось — генерируем фото вместо
                        log.warn("[Media] Video failed for post {}, falling back to image", current);
                        item.setContentType("фото");
                        doGenerateImage(item, niche, brand, mainCity, refBytes);
                    }
                } else {
                    doGenerateImage(item, niche, brand, mainCity, refBytes);
                }

                if (delayBetweenRequestsMs > 0)
                    Thread.sleep(delayBetweenRequestsMs);

            } catch (Exception e) {
                log.error("[Media] Error for post {}: {}", current, e.getMessage());
            }

            // Сохраняем прогресс после каждых 3 постов
            if (current % 3 == 0) {
                repository.save(strategy);
                log.info("[Media] Progress saved at post {}/{}", current, total);
            }
        }

        log.info("[Media] Done. Generated {} images, {} videos out of {} posts",
                total - videoGenerated, videoGenerated, total);
    }

    /**
     * Генерация видео через Veo 3 (с fallback на Veo 2).
     * Промпт строится из КОНКРЕТНОЙ темы и текста поста.
     * Возвращает true если видео успешно сгенерировано.
     */
    private boolean doGenerateVideo(MarketingStrategy.PostCalendarItem item,
            String niche, String brand, String city) {

        String theme = item.getTheme() != null ? item.getTheme() : "Product presentation";
        String postText = item.getPostText() != null ? item.getPostText() : "";
        String platform = item.getPlatform() != null
                ? item.getPlatform().toLowerCase()
                : "instagram";

        // Строим промпт на английском
        String englishPrompt = buildVideoPromptEnglish(theme, postText, niche, brand, city, platform);

        // Сохраняем для отладки
        item.setVideoGenerationPrompt(englishPrompt);

        log.info("[VEO3] Generating video:");
        log.info("[VEO3]   Platform : {}", platform);
        log.info("[VEO3]   Theme    : {}", theme);
        log.info("[VEO3]   Niche    : {} | Brand : {} | City : {}", niche, brand, city);
        log.info("[VEO3]   Prompt   : {}",
                englishPrompt.length() > 400
                        ? englishPrompt.substring(0, 400) + "..."
                        : englishPrompt);

        try {
            byte[] videoBytes = geminiVideoService.generateVideo(englishPrompt);

            if (videoBytes != null && videoBytes.length > 0) {
                String filename = "video_" + System.currentTimeMillis()
                        + "_" + Math.abs(item.hashCode()) + ".mp4";
                minIOService.uploadFile(filename, videoBytes, "video/mp4");
                item.setVideoUrl(filename);
                item.setVideoFilename(filename);
                item.setImageUrl(null);
                item.setImageFilename(null);
                log.info("[VEO3] ✅ Saved: {} ({} MB)",
                        filename, String.format("%.1f", videoBytes.length / 1024.0 / 1024.0));
                return true;
            } else {
                log.error("[VEO3] ❌ Empty response for theme: '{}'", theme);
                item.setVideoFilename(null);
                item.setVideoUrl(null);
                return false;
            }
        } catch (Exception e) {
            log.error("[VEO3] ❌ Exception for theme '{}': {}", theme, e.getMessage(), e);
            item.setVideoFilename(null);
            item.setVideoUrl(null);
            return false;
        }
    }

    /**
     * Строит промпт для Veo 3 на английском языке.
     *
     * Структура промпта:
     * [SCENE] → что происходит в кадре
     * [STYLE] → как снято, монтаж, операторская работа
     * [AUDIO] → звук (Veo 3 поддерживает нативно)
     * [RULES] → что запрещено (текст в кадре и т.д.)
     */
    private String buildVideoPromptEnglish(String theme, String postText,
            String niche, String brand,
            String city, String platform) {

        String speechScript = postText.length() > 250 ? postText.substring(0, 250).trim() : postText.trim();
        String sceneType = resolveSceneType(theme, niche);
        String style, audio;

        switch (platform) {
            case "tiktok" -> {
                style = "dynamic TikTok Reels style, fast energetic cuts, trendy transitions, handheld camera movement, authentic street style";
                audio = "upbeat modern music, energetic rhythm, suitable for young Kazakh audience";
            }
            case "telegram" -> {
                style = "clean professional look, steady camera, informative documentary style";
                audio = "calm soft background music, subtle ambient sounds, professional feel";
            }
            default -> {
                style = "cinematic Instagram Reels, smooth gimbal movement, warm color grading, aesthetic lifestyle composition, shallow depth of field";
                audio = "modern soft background music, atmospheric ambient, natural environment sounds";
            }
        }

        return String.format(
                "Vertical 9:16 cinematic video for %s business in %s, Kazakhstan. " +
                        "Business niche: %s. Brand: %s. " +
                        "Scene: %s. " +
                        "Visual concept: %s. " +
                        "People: Kazakh appearance, natural authentic look, real emotions, perfect anatomy, photorealistic. "
                        +
                        "Style: %s. " +
                        "Quality: 8K resolution, professional lighting, sharp focus, no blur, no CGI, photorealistic, commercial grade. "
                        +
                        "Sound design: %s. " +
                        "CRITICAL AUDIO INSTRUCTION: The narrator, voiceover, or characters MUST speak strictly in the RUSSIAN language with natural intonation. "
                        +
                        "Use the following Russian text as the exact spoken dialogue/voiceover script: \"%s\". " +
                        "STRICT RULES: NO text on screen. NO subtitles. NO captions. NO watermarks. NO on-screen graphics. NO logos overlaid. NO artificial CGI look.",
                platform, city, niche, brand, sceneType, theme, style, audio, speechScript);
    }

    /**
     * Определяет тип визуальной сцены по теме поста.
     * Переводит суть темы в описание конкретной сцены на английском.
     */
    private String resolveSceneType(String theme, String niche) {
        if (theme == null)
            return "business activity scene in " + niche;

        String t = theme.toLowerCase();

        // Кейсы и результаты
        if (t.contains("кейс") || t.contains("результат") || t.contains("успех") || t.contains("история"))
            return "happy satisfied client sharing their success story, before/after transformation";

        // Обучение и советы
        if (t.contains("совет") || t.contains("как") || t.contains("обучен") || t.contains("урок")
                || t.contains("разбор"))
            return "expert explaining key tips and techniques in " + niche + ", educational demonstration";

        // Процесс работы
        if (t.contains("процесс") || t.contains("работа") || t.contains("как мы") || t.contains("за кадром"))
            return "behind the scenes work process, professional team in action, " + niche + " workflow";

        // Отзывы
        if (t.contains("отзыв") || t.contains("клиент") || t.contains("мнение"))
            return "real client testimonial, satisfied customer talking about their experience";

        // Продажи / акции
        if (t.contains("акци") || t.contains("скидк") || t.contains("предложен") || t.contains("оффер"))
            return "attractive product/service showcase with special offer highlight, " + niche;

        // Демонстрация продукта
        if (t.contains("продукт") || t.contains("услуг") || t.contains("обзор") || t.contains("демонстрац"))
            return "detailed product/service demonstration, close-up shots, " + niche;

        // Команда
        if (t.contains("команда") || t.contains("специалист") || t.contains("эксперт"))
            return "professional team introduction, confident experts in " + niche + " environment";

        // Вопрос-ответ / вовлечение
        if (t.contains("вопрос") || t.contains("голосован") || t.contains("опрос"))
            return "engaging interactive moment, person asking audience a relatable question about " + niche;

        // По умолчанию — общая сцена из ниши
        return "authentic lifestyle scene showing " + niche + " activity, " +
                "real people benefiting from the service/product";
    }

    /**
     * Генерация изображений.
     * КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: промпт строится из темы И текста поста — картинка строго
     * в тему.
     */
    private void doGenerateImage(MarketingStrategy.PostCalendarItem item,
            String niche, String brand, String city, byte[] refBytes) {
        String imagePrompt = buildImagePrompt(item, niche, brand, city);
        log.info("[Image] Generating. Theme='{}'", item.getTheme());
        log.debug("[Image] Prompt: {}", imagePrompt);

        try {
            byte[] imageBytes = imageGenerationService.generateImageWithReference(imagePrompt, refBytes);
            if (imageBytes != null && imageBytes.length > 0) {
                String filename = "image_" + System.currentTimeMillis()
                        + "_" + Math.abs(item.hashCode()) + ".png";
                minIOService.uploadFile(filename, imageBytes, MediaType.IMAGE_PNG_VALUE);
                item.setImageUrl(filename);
                item.setImageFilename(filename);
                item.setVideoUrl(null);
                item.setVideoFilename(null);
                log.info("[Image] ✅ Saved: {}", filename);
            } else {
                log.warn("[Image] Empty response for: {}", item.getTheme());
                item.setImageFilename(null);
                item.setImageUrl(null);
            }
        } catch (Exception e) {
            log.error("[Image] Error for '{}': {}", item.getTheme(), e.getMessage());
            item.setImageFilename(null);
            item.setImageUrl(null);
        }
    }

    /**
     * ГЛАВНОЕ УЛУЧШЕНИЕ: промпт для изображения строится из КОНКРЕТНОЙ темы поста.
     * Картинка должна визуально иллюстрировать тему — а не быть случайным
     * портретом.
     */
    private String buildImagePrompt(MarketingStrategy.PostCalendarItem item,
            String niche, String brand, String city) {
        String theme = item.getTheme() != null ? item.getTheme() : "";
        String postText = item.getPostText() != null ? item.getPostText() : "";
        String platform = item.getPlatform() != null ? item.getPlatform().toLowerCase() : "instagram";

        // Берём первые 150 символов текста поста для контекста
        String textContext = postText.length() > 150 ? postText.substring(0, 150) : postText;

        StringBuilder prompt = new StringBuilder();

        // Базовый стиль по платформе
        switch (platform) {
            case "tiktok" -> prompt.append(
                    "Яркая динамичная фотография 8K, трендовый молодёжный стиль, " +
                            "насыщенные цвета, энергичная композиция, высокий контраст. ");
            case "linkedin" -> prompt.append(
                    "Профессиональная корпоративная фотография 8K, деловой стиль, " +
                            "чистый минималистичный дизайн, мягкое естественное освещение. ");
            default -> prompt.append(
                    "Кинематографичная коммерческая фотография 8K высокого класса, " +
                            "профессиональное освещение, эстетика Instagram, современный казахстанский lifestyle. ");
        }

        // КОНКРЕТНАЯ ТЕМА — это главное для релевантности изображения
        prompt.append(String.format(
                "Ниша: %s. Бренд: %s. Город: %s. ", niche, brand, city));

        if (!theme.isBlank()) {
            prompt.append(String.format("ГЛАВНАЯ ТЕМА ИЗОБРАЖЕНИЯ: «%s». ", theme));
        }

        // Используем контекст текста поста для точности
        if (!textContext.isBlank()) {
            prompt.append(String.format("Контекст: %s. ", textContext));
        }

        prompt.append(
                "Показывай КОНКРЕТНУЮ сцену из ниши — действие, процесс, результат, продукт. " +
                        "Люди казахстанской внешности, естественные позы и эмоции, реальная жизнь. " +
                        "Продукт или услуга в главном фокусе. " +
                        "БЕЗ текстовых надписей. БЕЗ водяных знаков. Фотореалистично.");

        return prompt.toString();
    }

    // =====================================================================
    // REGENERATION
    // =====================================================================

    @SuppressWarnings("unchecked")
    public MarketingStrategy.PostCalendarItem regeneratePostImage(String strategyId, int postIndex) {
        MarketingStrategy strategy = repository.findById(strategyId).orElse(null);
        if (strategy == null)
            return null;
        List<MarketingStrategy.PostCalendarItem> calendar = strategy.getPostCalendar();
        if (calendar == null || postIndex < 0 || postIndex >= calendar.size())
            return null;

        MarketingStrategy.PostCalendarItem item = calendar.get(postIndex);
        item.setContentType("фото");

        MarketingAnalysisV3Document analysis = analysisRepository
                .findById(strategy.getAnalysisId()).orElse(null);
        if (analysis == null)
            return null;

        String niche = safeGet(analysis, a -> a.getRequestData().getBusinessNiche(), "бизнес");
        String brand = safeGet(analysis, a -> a.getRequestData().getProductName(), "");
        String mainCity = safeGet(analysis, a -> {
            String c = a.getRequestData().getMainCity();
            return (c != null && !c.isBlank()) ? c : "Алматы";
        }, "Алматы");

        byte[] refBytes = null;
        if (strategy.getStrategyData() != null
                && strategy.getStrategyData().containsKey("referenceFilenames")) {
            List<String> refFiles = (List<String>) strategy.getStrategyData().get("referenceFilenames");
            if (refFiles != null && !refFiles.isEmpty()) {
                String randomRef = refFiles.get(new Random().nextInt(refFiles.size()));
                try (InputStream s = minIOService.downloadFile(randomRef)) {
                    refBytes = s.readAllBytes();
                } catch (Exception e) {
                    log.warn("[Regen] Failed to load ref: {}", e.getMessage());
                }
            }
        }

        doGenerateImage(item, niche, brand, mainCity, refBytes);
        repository.save(strategy);
        return item;
    }

    @Async("reportGenerationExecutor")
    public void regeneratePostVideoAsync(String strategyId, int postIndex) {
        try {
            MarketingStrategy strategy = repository.findById(strategyId).orElse(null);
            if (strategy == null) {
                log.error("[VideoRegen] Strategy {} not found", strategyId);
                return;
            }

            List<MarketingStrategy.PostCalendarItem> calendar = strategy.getPostCalendar();
            if (calendar == null || postIndex < 0 || postIndex >= calendar.size()) {
                log.error("[VideoRegen] Invalid postIndex {}", postIndex);
                return;
            }

            MarketingStrategy.PostCalendarItem item = calendar.get(postIndex);
            item.setContentType("видео");
            item.setVideoUrl("generating...");
            item.setVideoFilename(null);
            item.setImageUrl(null);
            item.setImageFilename(null);
            repository.save(strategy);

            MarketingAnalysisV3Document analysis = analysisRepository
                    .findById(strategy.getAnalysisId()).orElse(null);
            if (analysis == null) {
                log.error("[VideoRegen] Analysis not found");
                return;
            }

            String niche = safeGet(analysis, a -> a.getRequestData().getBusinessNiche(), "бизнес");
            String brand = safeGet(analysis, a -> a.getRequestData().getProductName(), "");
            String mainCity = safeGet(analysis, a -> {
                String c = a.getRequestData().getMainCity();
                return (c != null && !c.isBlank()) ? c : "Алматы";
            }, "Алматы");

            boolean success = doGenerateVideo(item, niche, brand, mainCity);
            if (!success) {
                // Если видео не получилось — генерируем фото
                log.warn("[VideoRegen] Video failed, generating image instead for post {}", postIndex);
                item.setContentType("фото");
                doGenerateImage(item, niche, brand, mainCity, null);
            }

            repository.save(strategy);
            log.info("[VideoRegen] Done for post {} in strategy {}", postIndex, strategyId);
        } catch (Exception e) {
            log.error("[VideoRegen] Failed: {}", e.getMessage(), e);
        }
    }

    // =====================================================================
    // STRATEGY RESULT
    // =====================================================================

    public Map<String, Object> getStrategyResult(String strategyId) {
        MarketingStrategy strategy = repository.findById(strategyId).orElse(null);
        if (strategy == null)
            return null;

        Map<String, Object> response = new LinkedHashMap<>();
        response.put("strategyId", strategy.getId());
        response.put("analysisId", strategy.getAnalysisId());
        response.put("status", strategy.getStatus());
        response.put("createdAt", strategy.getCreatedAt());
        response.put("completedAt", strategy.getCompletedAt());
        response.put("durationWeeks", strategy.getDurationWeeks());
        response.put("priorityPlatforms", strategy.getPriorityPlatforms());

        // Название модели — из entity, резерв из strategyData
        String modelName = strategy.getScoringModelName();
        String modelTitle = strategy.getScoringModelTitle();
        if (modelName == null && strategy.getStrategyData() != null) {
            Object v = strategy.getStrategyData().get("scoringModel");
            if (v != null)
                modelName = v.toString();
        }
        if (modelTitle == null && strategy.getStrategyData() != null) {
            Object v = strategy.getStrategyData().get("scoringModelTitle");
            if (v != null)
                modelTitle = v.toString();
        }
        response.put("scoringModelName", modelName);
        response.put("scoringModelTitle", modelTitle);
        response.put("strategyData", strategy.getStrategyData());

        if (strategy.getWeeklyPlans() != null && strategy.getPostCalendar() != null) {
            List<PostingTask> tasks = postingTaskService.getStrategyTasks(strategyId);
            List<Map<String, Object>> postCalendarList = new ArrayList<>();
            List<MarketingStrategy.PostCalendarItem> items = strategy.getPostCalendar();

            for (int i = 0; i < items.size(); i++) {
                MarketingStrategy.PostCalendarItem item = items.get(i);
                Map<String, Object> dto = new LinkedHashMap<>();
                dto.put("postIndex", i);
                dto.put("publishDate", item.getPublishDate());
                dto.put("weekNumber", item.getWeekNumber());
                dto.put("platform", item.getPlatform());
                dto.put("contentType", item.getContentType());
                dto.put("theme", item.getTheme());
                dto.put("postText", item.getPostText());
                dto.put("hashtags", item.getHashtags());
                dto.put("publishTime", item.getPublishTime());
                dto.put("imageUrl", item.getImageUrl());
                dto.put("imageFilename", item.getImageFilename());
                dto.put("videoUrl", item.getVideoUrl());
                dto.put("videoFilename", item.getVideoFilename());

                tasks.stream()
                        .filter(t -> t.getPublishDate() != null && item.getPublishDate() != null
                                && t.getPublishDate().equals(item.getPublishDate())
                                && t.getPlatform() != null && item.getPlatform() != null
                                && t.getPlatform().equalsIgnoreCase(item.getPlatform()))
                        .findFirst()
                        .ifPresentOrElse(
                                t -> {
                                    dto.put("taskId", t.getId());
                                    dto.put("taskStatus", t.getStatus());
                                },
                                () -> dto.put("taskStatus", null));

                postCalendarList.add(dto);
            }

            response.put("strategy", Map.of("weeklyPlans", strategy.getWeeklyPlans(),
                    "postCalendar", postCalendarList));
            response.put("postCalendar", postCalendarList);
            response.put("weeklyPlans", strategy.getWeeklyPlans());
        }

        return response;
    }

    // =====================================================================
    // HELPERS
    // =====================================================================

    @FunctionalInterface
    private interface AnalysisExtractor<T> {
        T extract(MarketingAnalysisV3Document a);
    }

    private <T> T safeGet(MarketingAnalysisV3Document analysis,
            AnalysisExtractor<T> extractor, T defaultValue) {
        try {
            if (analysis == null || analysis.getRequestData() == null)
                return defaultValue;
            T val = extractor.extract(analysis);
            return val != null ? val : defaultValue;
        } catch (Exception e) {
            return defaultValue;
        }
    }

    @SuppressWarnings("unchecked")
    private void populateStrategyEntity(MarketingStrategy strategy,
            Map<String, Object> data,
            ScoringResult scoring) {
        strategy.setStrategyData(data);
        strategy.setScoringModelName(scoring.getModel().name());
        strategy.setScoringModelTitle(scoring.getModel().getTitle());

        if (data.containsKey("recommendedDurationWeeks"))
            strategy.setDurationWeeks((Integer) data.get("recommendedDurationWeeks"));
        if (data.containsKey("recommendedPlatforms"))
            strategy.setPriorityPlatforms((List<String>) data.get("recommendedPlatforms"));

        // ===== ОБРАБОТКА НЕДЕЛЬНЫХ ПЛАНОВ =====
        List<Map<String, Object>> weeksRaw = (List<Map<String, Object>>) data.get("weeklyPlans");
        List<MarketingStrategy.WeeklyPlan> weeklyPlans = new ArrayList<>();
        if (weeksRaw != null) {
            for (Map<String, Object> w : weeksRaw) {
                MarketingStrategy.WeeklyPlan plan = new MarketingStrategy.WeeklyPlan();
                plan.setWeekNumber((Integer) w.get("weekNumber"));
                plan.setMainThemes((List<String>) w.get("mainThemes"));
                plan.setContentRecommendations((String) w.get("contentRecommendations"));
                plan.setPriorityPlatforms((List<String>) w.get("priorityPlatforms"));
                weeklyPlans.add(plan);
            }
        }
        strategy.setWeeklyPlans(weeklyPlans);

        // ===== ОБРАБОТКА КАЛЕНДАРЯ ПОСТОВ =====
        List<Map<String, Object>> calendarRaw = (List<Map<String, Object>>) data.get("postCalendar");
        List<MarketingStrategy.PostCalendarItem> postCalendar = new ArrayList<>();
        LocalDateTime startDate = LocalDateTime.now().plusDays(1);

        if (calendarRaw != null && !calendarRaw.isEmpty()) {
            // Валидация: если постов меньше 12, логируем предупреждение
            if (calendarRaw.size() < 12) {
                log.warn("[Strategy] AI вернул только {} постов (ожидалось 14-16). Применяю нормализацию...",
                        calendarRaw.size());
            }

            int postIndex = 0;
            for (Map<String, Object> c : calendarRaw) {
                MarketingStrategy.PostCalendarItem item = new MarketingStrategy.PostCalendarItem();

                // Извлекаем weekNumber из ответа AI (если есть)
                Integer weekNumber = c.get("weekNumber") != null
                        ? ((Number) c.get("weekNumber")).intValue()
                        : null;

                // Обработка даты
                String dateStr = (String) c.get("publishDate");
                LocalDateTime publishDate;

                if (dateStr != null) {
                    try {
                        publishDate = LocalDateTime.parse(dateStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
                    } catch (Exception e) {
                        log.warn("[Strategy] Невалидная дата '{}', генерирую автоматически", dateStr);
                        publishDate = null;
                    }
                } else {
                    publishDate = null;
                }

                // Если даты нет или она некорректна — рассчитываем по позиции
                if (publishDate == null) {
                    int weekNum = weekNumber != null ? weekNumber : (postIndex / 4) + 1;
                    int dayInWeek = postIndex % 4;
                    int dayOffset = (weekNum - 1) * 7 + getDayOffset(dayInWeek);
                    publishDate = startDate.plusDays(dayOffset);
                }

                item.setPublishDate(publishDate);
                item.setWeekNumber(weekNumber != null ? weekNumber : (postIndex / 4) + 1);

                // Нормализуем contentType
                String ct = (String) c.get("contentType");
                if (ct != null)
                    ct = ct.trim().toLowerCase();
                item.setContentType("видео".equals(ct) ? "видео" : "фото");

                item.setPlatform((String) c.get("platform"));
                item.setTheme((String) c.get("theme"));
                item.setPostText((String) c.get("postText"));
                item.setHashtags((List<String>) c.get("hashtags"));
                item.setPublishTime((String) c.get("publishTime"));

                postCalendar.add(item);
                postIndex++;
            }
        }

        // ===== НОРМАЛИЗАЦИЯ РАСПИСАНИЯ =====
        normalizePostSchedule(postCalendar, startDate);

        strategy.setPostCalendar(postCalendar);

        log.info("Populated: {} weeklyPlans, {} posts, model={}, videos={}",
                weeklyPlans.size(), postCalendar.size(), scoring.getModel().name(),
                postCalendar.stream().filter(p -> "видео".equals(p.getContentType())).count());
    }

    /**
     * Нормализует расписание постов — равномерное распределение по дням.
     * Гарантирует: 4 поста в неделю, публикации в Пн-Ср-Пт-Сб.
     */
    private void normalizePostSchedule(List<MarketingStrategy.PostCalendarItem> postCalendar,
            LocalDateTime startDate) {
        if (postCalendar == null || postCalendar.isEmpty())
            return;

        int totalPosts = postCalendar.size();
        int weeksCount = (int) Math.ceil(totalPosts / 4.0);

        // Время публикации для каждого поста в неделе: Пн 09:00, Ср 12:00, Пт 18:00, Сб
        // 20:00
        String[] weekTimeSlots = { "09:00", "12:00", "18:00", "20:00" };
        int[] dayOffsets = { 0, 2, 4, 5 }; // Пн=0, Ср=2, Пт=4, Сб=5

        for (int i = 0; i < postCalendar.size(); i++) {
            MarketingStrategy.PostCalendarItem item = postCalendar.get(i);

            // Определяем неделю и позицию в неделе
            int weekNum = (i / 4) + 1;
            int posInWeek = i % 4;

            // Устанавливаем weekNumber
            item.setWeekNumber(weekNum);

            // Рассчитываем дату
            int dayOffset = (weekNum - 1) * 7 + dayOffsets[posInWeek];
            LocalDateTime newDate = startDate.plusDays(dayOffset);

            // Если исходная дата была валидной и в пределах +/- 2 дней от новой, сохраняем
            // её
            // Иначе используем рассчитанную дату для равномерности
            if (item.getPublishDate() != null) {
                long daysDiff = java.time.Duration.between(item.getPublishDate(), newDate).toDays();
                if (Math.abs(daysDiff) > 2) {
                    item.setPublishDate(newDate);
                }
            } else {
                item.setPublishDate(newDate);
            }

            // Устанавливаем время публикации
            item.setPublishTime(weekTimeSlots[posInWeek]);

            log.debug("[Normalize] Post {}: week={}, posInWeek={}, date={}, time={}",
                    i, weekNum, posInWeek, item.getPublishDate(), item.getPublishTime());
        }
    }

    /**
     * Возвращает смещение дня в неделе для позиции поста (0-3).
     * 0=Пн, 1=Ср, 2=Пт, 3=Сб
     */
    private int getDayOffset(int posInWeek) {
        switch (posInWeek) {
            case 0:
                return 0; // Понедельник
            case 1:
                return 2; // Среда
            case 2:
                return 4; // Пятница
            case 3:
                return 5; // Суббота
            default:
                return 0;
        }
    }

    private String extractCleanJson(String response) {
        if (response == null || response.isBlank())
            return null;
        String s = response.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('}');
        return (first >= 0 && last > first) ? s.substring(first, last + 1) : null;
    }

    private void addStatusHistory(MarketingStrategy strategy, String status, String message) {
        if (strategy.getStatusHistory() == null)
            strategy.setStatusHistory(new ArrayList<>());
        strategy.getStatusHistory().add(new StatusHistoryEntry(status, LocalDateTime.now(), message));
    }

    private void markAsFailed(String strategyId, String errorMsg) {
        repository.findById(strategyId).ifPresent(s -> {
            s.setStatus("failed");
            addStatusHistory(s, "failed", errorMsg);
            repository.save(s);
        });
    }

    // =====================================================================
    // FACEBOOK DIRECT PUBLISH
    // =====================================================================

    public String publishFirstFacebookPost(String strategyId) {
        Optional<MarketingStrategy> optStrategy = repository.findById(strategyId);
        if (optStrategy.isEmpty()) {
            throw new IllegalArgumentException("Strategy not found: " + strategyId);
        }

        MarketingStrategy strategy = optStrategy.get();
        if (strategy.getPostCalendar() == null || strategy.getPostCalendar().isEmpty()) {
            throw new IllegalStateException("Strategy has no posts in calendar");
        }

        Optional<MarketingStrategy.PostCalendarItem> fbPostOpt = strategy.getPostCalendar().stream()
                .findFirst();

        if (fbPostOpt.isEmpty()) {
            throw new IllegalStateException("No posts found in the strategy calendar");
        }

        MarketingStrategy.PostCalendarItem fbPost = fbPostOpt.get();

        // Пробуем загрузить видео — приоритет над фото
        byte[] videoData = null;
        if (fbPost.getVideoFilename() != null && !fbPost.getVideoFilename().isEmpty()) {
            try (java.io.InputStream videoStream = minIOService.downloadFile(fbPost.getVideoFilename())) {
                videoData = videoStream.readAllBytes();
                log.info("[Facebook] Загружено видео из MinIO: {} ({} bytes)", fbPost.getVideoFilename(),
                        videoData.length);
            } catch (Exception e) {
                log.warn("[Facebook] Не удалось загрузить видео {}: {}", fbPost.getVideoFilename(), e.getMessage());
            }
        }

        byte[] imageData = null;
        if (videoData == null && fbPost.getImageFilename() != null && !fbPost.getImageFilename().isEmpty()) {
            try (java.io.InputStream imageStream = minIOService.downloadFile(fbPost.getImageFilename())) {
                imageData = imageStream.readAllBytes();
            } catch (Exception e) {
                log.warn("Failed to load image for post: {}", e.getMessage());
            }
        }

        String postId;
        if (videoData != null && videoData.length > 0) {
            log.info("[Facebook] Публикую первый пост с ВИДЕО");
            postId = facebookPostingService.postToPageWithVideo(fbPost.getPostText(), fbPost.getHashtags(), videoData);
        } else if (imageData != null && imageData.length > 0) {
            postId = facebookPostingService.postToPageWithImage(fbPost.getPostText(), fbPost.getHashtags(), imageData);
        } else {
            postId = facebookPostingService.postToPage(fbPost.getPostText(), fbPost.getHashtags());
        }

        if (postId != null) {
            try {
                String reportText = "✅ НОВЫЙ ПОСТ ОПУБЛИКОВАН!\n\n" +
                        "📜 Текст поста:\n" + fbPost.getPostText() + "\n\n" +
                        "🔗 Ссылка: https://facebook.com/" + postId + "\n" +
                        "🕒 Время: " + LocalDateTime.now();
                facebookPostingService.sendPrivateMessage("25769470256007187", reportText); // TEST_RECIPIENT_ID
                if (imageData != null && imageData.length > 0) {
                    facebookPostingService.sendPrivateImageMessage("25769470256007187", imageData);
                }
            } catch (Exception msgEx) {
                log.warn("Post published but failed to send private notification: {}", msgEx.getMessage());
            }
        }

        return postId;
    }
}