package com.yaoai.pipeline.service.impl;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yaoai.ai.core.model.ChatMessage;
import com.yaoai.ai.core.model.ChatRequest;
import com.yaoai.ai.core.service.LlmService;
import com.yaoai.ai.providers.service.SeedreamService;
import com.yaoai.billing.dto.BillingChargeRequest;
import com.yaoai.billing.service.BillingService;
import com.yaoai.common.context.UserContext;
import com.yaoai.common.exception.BizException;
import com.yaoai.common.exception.ErrorCode;
import com.yaoai.domain.entity.Character;
import com.yaoai.domain.entity.Episode;
import com.yaoai.domain.entity.Outline;
import com.yaoai.domain.entity.Project;
import com.yaoai.domain.entity.Scene;
import com.yaoai.domain.mapper.CharacterMapper;
import com.yaoai.domain.mapper.EpisodeMapper;
import com.yaoai.domain.mapper.OutlineMapper;
import com.yaoai.domain.mapper.ProjectMapper;
import com.yaoai.domain.mapper.SceneMapper;
import com.yaoai.pipeline.service.AssetGenPipelineService;
import com.yaoai.storage.service.TosService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

@Slf4j
@Service
@RequiredArgsConstructor
public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {

    private static final String CHARACTER_SYSTEM = """
            你是影视短剧角色设定专家。请根据给定的大纲和分集信息，提取主要角色并输出 JSON 数组。
            返回格式如下：
            [
              {
                "name": "角色姓名",
                "role_type": "female_lead / male_lead / antagonist / supporting",
                "gender": "male or female",
                "age": "年龄描述",
                "personality": "性格特征，30字以内",
                "costume": "主要服装描述，30字以内",
                "visual_hint": "外貌特征，30字以内",
                "image_prompt": "英文角色设定图 prompt，突出服装、发型、体型、气质，用于统一角色三视图生成"
              }
            ]
            只返回 3 到 8 个主要角色，只返回 JSON。
            """;

    private static final String SCENE_SYSTEM = """
            你是影视短剧场景设定专家。请根据给定的大纲和分集信息，提取主要场景并输出 JSON 数组。
            返回格式如下：
            [
              {
                "name": "场景名称",
                "scene_type": "indoor or outdoor",
                "description": "场景描述，30字以内",
                "image_prompt": "英文场景图 prompt，电影感环境描述"
              }
            ]
            只返回 3 到 6 个主要场景，只返回 JSON。
            """;

    private final LlmService llmService;
    private final SeedreamService seedreamService;
    private final TosService tosService;
    private final CharacterMapper characterMapper;
    private final SceneMapper sceneMapper;
    private final OutlineMapper outlineMapper;
    private final EpisodeMapper episodeMapper;
    private final ProjectMapper projectMapper;
    private final ObjectMapper objectMapper;
    private final BillingService billingService;
    private final HttpClient httpClient = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();

    @Override
    public List<Character> extractCharacters(Long projectId, Long tenantId) {
        Project project = loadProject(projectId, tenantId);
        ProjectVisualStyle visualStyle = resolveProjectVisualStyle(project);
        String context = buildCharacterExtractionContext(projectId, tenantId, visualStyle);
        log.info("Extracting characters: projectId={}", projectId);

        billingService.checkBalance(BillingChargeRequest.builder()
                .tenantId(tenantId)
                .userId(UserContext.require())
                .projectId(projectId)
                .operation("character_extract")
                .modality("TEXT")
                .modelProvider(llmService.getProvider())
                .modelId(llmService.getModelId())
                .modelName(llmService.getModelName())
                .unitCount(1)
                .refId(String.valueOf(projectId))
                .build());

        String raw = llmService.chat(ChatRequest.builder()
                .messages(List.of(ChatMessage.system(CHARACTER_SYSTEM), ChatMessage.user(context)))
                .build());

        try {
            List<Map<String, Object>> items = objectMapper.readValue(extractJson(raw), new TypeReference<>() {});
            List<Character> result = new ArrayList<>();
            for (Map<String, Object> item : items) {
                Character character = new Character();
                character.setProjectId(projectId);
                character.setTenantId(tenantId);
                character.setName((String) item.getOrDefault("name", ""));
                character.setRoleType((String) item.getOrDefault("role_type", ""));
                character.setGender((String) item.getOrDefault("gender", ""));
                character.setAge((String) item.getOrDefault("age", ""));
                character.setPersonality((String) item.getOrDefault("personality", ""));
                character.setCostume((String) item.getOrDefault("costume", ""));
                character.setVisualHint((String) item.getOrDefault("visual_hint", ""));
                character.setImagePrompt((String) item.getOrDefault("image_prompt", ""));
                character.setStatus("draft");
                characterMapper.insert(character);
                result.add(character);
            }
            billingService.charge(BillingChargeRequest.builder()
                    .tenantId(tenantId)
                    .userId(UserContext.require())
                    .projectId(projectId)
                    .operation("character_extract")
                    .modality("TEXT")
                    .modelProvider(llmService.getProvider())
                    .modelId(llmService.getModelId())
                    .modelName(llmService.getModelName())
                    .unitCount(result.size())
                    .refId(String.valueOf(projectId))
                    .build());
            return result;
        } catch (Exception e) {
            log.error("Character extraction failed", e);
            throw new BizException(ErrorCode.INTERNAL_ERROR, "角色提取失败: " + e.getMessage());
        }
    }

    @Override
    public List<Scene> extractScenes(Long projectId, Long tenantId) {
        String context = buildProjectContext(projectId, tenantId);
        log.info("Extracting scenes: projectId={}", projectId);

        billingService.checkBalance(BillingChargeRequest.builder()
                .tenantId(tenantId)
                .userId(UserContext.require())
                .projectId(projectId)
                .operation("scene_extract")
                .modality("TEXT")
                .modelProvider(llmService.getProvider())
                .modelId(llmService.getModelId())
                .modelName(llmService.getModelName())
                .unitCount(1)
                .refId(String.valueOf(projectId))
                .build());

        String raw = llmService.chat(ChatRequest.builder()
                .messages(List.of(ChatMessage.system(SCENE_SYSTEM), ChatMessage.user(context)))
                .build());

        try {
            List<Map<String, Object>> items = objectMapper.readValue(extractJson(raw), new TypeReference<>() {});
            List<Scene> result = new ArrayList<>();
            for (Map<String, Object> item : items) {
                Scene scene = new Scene();
                scene.setProjectId(projectId);
                scene.setTenantId(tenantId);
                scene.setName((String) item.getOrDefault("name", ""));
                scene.setSceneType((String) item.getOrDefault("scene_type", "indoor"));
                scene.setDescription((String) item.getOrDefault("description", ""));
                scene.setImagePrompt((String) item.getOrDefault("image_prompt", ""));
                scene.setStatus("draft");
                sceneMapper.insert(scene);
                result.add(scene);
            }
            billingService.charge(BillingChargeRequest.builder()
                    .tenantId(tenantId)
                    .userId(UserContext.require())
                    .projectId(projectId)
                    .operation("scene_extract")
                    .modality("TEXT")
                    .modelProvider(llmService.getProvider())
                    .modelId(llmService.getModelId())
                    .modelName(llmService.getModelName())
                    .unitCount(result.size())
                    .refId(String.valueOf(projectId))
                    .build());
            return result;
        } catch (Exception e) {
            log.error("Scene extraction failed", e);
            throw new BizException(ErrorCode.INTERNAL_ERROR, "场景提取失败: " + e.getMessage());
        }
    }

    @Override
    public Character generateCharacterImage(Long characterId, Long tenantId) {
        Character character = requireCharacter(characterId, tenantId);
        Project project = loadProject(character.getProjectId(), tenantId);
        ProjectVisualStyle visualStyle = resolveProjectVisualStyle(project);
        if (character.getImagePrompt() == null || character.getImagePrompt().isBlank()) {
            character.setImagePrompt(buildCharacterImagePromptFallback(character));
        }
        if (character.getImagePrompt() == null || character.getImagePrompt().isBlank()) {
            throw new BizException(ErrorCode.INVALID_PARAM, "角色缺少图片 Prompt，且无法从角色资料推导");
        }

        billingService.checkBalance(BillingChargeRequest.builder()
                .tenantId(tenantId)
                .userId(UserContext.require())
                .projectId(character.getProjectId())
                .operation("character_image_generate")
                .modality("IMAGE")
                .modelProvider(seedreamService.getProvider())
                .modelId(seedreamService.getModelId())
                .modelName(seedreamService.getModelName())
                .unitCount(1)
                .meta(Map.of("layout", "sheet"))
                .refId(String.valueOf(characterId))
                .build());

        character.setStatus("generating");
        characterMapper.updateById(character);

        try {
            String prompt = buildCharacterSheetPrompt(character.getImagePrompt(), visualStyle);
            // 角色三视图采用 16:9 横版画布，承载“大头像 + 正/侧/背 全身”一排四联布局
            // 注意：Seedream 要求像素数 ≥ 3,686,400，2560x1440 = 3,686,400 恰好满足且为标准 16:9
            String generatedUrl = seedreamService.generateImage(prompt, "2560x1440");
            byte[] bytes = downloadBytes(generatedUrl);
            String key = TosService.buildKey(
                    character.getTenantId(),
                    character.getProjectId(),
                    "characters/" + character.getId(),
                    "sheet.jpg"
            );
            tosService.upload(key, new ByteArrayInputStream(bytes), bytes.length, "image/jpeg");

            String publicUrl = tosService.publicUrl(key);
            // 拼图同时写入 imageUrl 与 frontImage* 字段：
            // - imageUrl 是卡片主图
            // - frontImage* 是 storyboard 引用兜底（StoryboardWorkspace.tsx 用到 frontImageUrl）
            character.setImageUrl(publicUrl);
            character.setImageTosKey(key);
            character.setFrontImageUrl(publicUrl);
            character.setFrontImageTosKey(key);
            character.setSideImageUrl(null);
            character.setSideImageTosKey(null);
            character.setBackImageUrl(null);
            character.setBackImageTosKey(null);

            character.setStatus("ready");
            characterMapper.updateById(character);
            billingService.charge(BillingChargeRequest.builder()
                    .tenantId(tenantId)
                    .userId(UserContext.require())
                    .projectId(character.getProjectId())
                    .operation("character_image_generate")
                    .modality("IMAGE")
                    .modelProvider(seedreamService.getProvider())
                    .modelId(seedreamService.getModelId())
                    .modelName(seedreamService.getModelName())
                    .unitCount(1)
                    .meta(Map.of("layout", "sheet"))
                    .refId(String.valueOf(characterId))
                    .build());
            return character;
        } catch (BizException e) {
            character.setStatus("failed");
            characterMapper.updateById(character);
            throw e;
        } catch (Exception e) {
            character.setStatus("failed");
            characterMapper.updateById(character);
            throw new BizException(ErrorCode.INTERNAL_ERROR, "角色三视图生成失败: " + e.getMessage());
        }
    }

    @Override
    public Scene generateSceneImage(Long sceneId, Long tenantId) {
        Scene scene = sceneMapper.selectById(sceneId);
        if (scene == null || !tenantId.equals(scene.getTenantId())) {
            throw new BizException(ErrorCode.NOT_FOUND, "场景不存在");
        }
        if (scene.getImagePrompt() == null || scene.getImagePrompt().isBlank()) {
            scene.setImagePrompt(buildSceneImagePromptFallback(scene));
        }
        if (scene.getImagePrompt() == null || scene.getImagePrompt().isBlank()) {
            throw new BizException(ErrorCode.INVALID_PARAM, "场景缺少图片 Prompt");
        }

        billingService.checkBalance(BillingChargeRequest.builder()
                .tenantId(tenantId)
                .userId(UserContext.require())
                .projectId(scene.getProjectId())
                .operation("scene_image_generate")
                .modality("IMAGE")
                .modelProvider(seedreamService.getProvider())
                .modelId(seedreamService.getModelId())
                .modelName(seedreamService.getModelName())
                .unitCount(1)
                .refId(String.valueOf(sceneId))
                .build());

        scene.setStatus("generating");
        sceneMapper.updateById(scene);

        try {
            String imageUrl = seedreamService.generateImage(scene.getImagePrompt());
            byte[] bytes = downloadBytes(imageUrl);
            String key = TosService.buildKey(scene.getTenantId(), scene.getProjectId(), "scenes", scene.getName() + ".jpg");
            tosService.upload(key, new ByteArrayInputStream(bytes), bytes.length, "image/jpeg");

            scene.setImageUrl(tosService.publicUrl(key));
            scene.setImageTosKey(key);
            scene.setStatus("ready");
            sceneMapper.updateById(scene);
            billingService.charge(BillingChargeRequest.builder()
                    .tenantId(tenantId)
                    .userId(UserContext.require())
                    .projectId(scene.getProjectId())
                    .operation("scene_image_generate")
                    .modality("IMAGE")
                    .modelProvider(seedreamService.getProvider())
                    .modelId(seedreamService.getModelId())
                    .modelName(seedreamService.getModelName())
                    .unitCount(1)
                    .refId(String.valueOf(sceneId))
                    .build());
            return scene;
        } catch (BizException e) {
            scene.setStatus("failed");
            sceneMapper.updateById(scene);
            throw e;
        } catch (Exception e) {
            scene.setStatus("failed");
            sceneMapper.updateById(scene);
            throw new BizException(ErrorCode.INTERNAL_ERROR, "场景图片生成失败: " + e.getMessage());
        }
    }

    @Override
    public List<Character> listCharacters(Long projectId, Long tenantId) {
        return characterMapper.findByProject(projectId, tenantId);
    }

    @Override
    public List<Scene> listScenes(Long projectId, Long tenantId) {
        return sceneMapper.findByProject(projectId, tenantId);
    }

    @Override
    public Character saveCharacter(Character character) {
        if (character.getImagePrompt() == null || character.getImagePrompt().isBlank()) {
            character.setImagePrompt(buildCharacterImagePromptFallback(character));
        }
        if (character.getId() == null) {
            characterMapper.insert(character);
        } else {
            characterMapper.updateById(character);
        }
        return character;
    }

    private String buildCharacterImagePromptFallback(Character character) {
        if (character == null) {
            return "";
        }
        List<String> parts = new ArrayList<>();
        appendIfPresent(parts, character.getName());
        if (character.getGender() != null) {
            String gender = character.getGender().trim().toLowerCase(Locale.ROOT);
            if ("male".equals(gender)) {
                parts.add("male character");
            } else if ("female".equals(gender)) {
                parts.add("female character");
            }
        }
        appendIfPresent(parts, character.getAge());
        appendIfPresent(parts, character.getPersonality());
        appendIfPresent(parts, character.getCostume());
        appendIfPresent(parts, character.getVisualHint());
        if (parts.isEmpty()) {
            return "";
        }
        return String.join(", ", parts);
    }

    private void appendIfPresent(List<String> parts, String value) {
        if (value != null && !value.isBlank()) {
            parts.add(value.trim());
        }
    }

    @Override
    public Scene saveScene(Scene scene) {
        if (scene.getId() != null) {
            Scene existing = sceneMapper.selectById(scene.getId());
            if (existing != null && shouldRefreshSceneImagePrompt(scene, existing)) {
                scene.setImagePrompt(buildSceneImagePromptFallback(scene));
            }
        }
        if (scene.getImagePrompt() == null || scene.getImagePrompt().isBlank()) {
            scene.setImagePrompt(buildSceneImagePromptFallback(scene));
        }
        if (scene.getId() == null) {
            sceneMapper.insert(scene);
        } else {
            sceneMapper.updateById(scene);
        }
        return scene;
    }

    private String buildSceneImagePromptFallback(Scene scene) {
        if (scene == null) {
            return "";
        }
        StringBuilder prompt = new StringBuilder();
        if (scene.getName() != null && !scene.getName().isBlank()) {
            prompt.append(scene.getName().trim());
        }
        if (scene.getDescription() != null && !scene.getDescription().isBlank()) {
            if (prompt.length() > 0) {
                prompt.append(", ");
            }
            prompt.append(scene.getDescription().trim());
        }
        if (scene.getSceneType() != null && !scene.getSceneType().isBlank()) {
            if (prompt.length() > 0) {
                prompt.append(", ");
            }
            prompt.append("indoor".equalsIgnoreCase(scene.getSceneType()) ? "indoor scene" : "outdoor scene");
        }
        return prompt.toString();
    }

    private boolean shouldRefreshSceneImagePrompt(Scene incoming, Scene existing) {
        if (incoming == null || existing == null) {
            return false;
        }
        String incomingPrompt = normalizeSceneField(incoming.getImagePrompt());
        String existingPrompt = normalizeSceneField(existing.getImagePrompt());
        if (incomingPrompt.isBlank()) {
            return true;
        }
        if (!incomingPrompt.equals(existingPrompt)) {
            return false;
        }
        return !normalizeSceneField(incoming.getName()).equals(normalizeSceneField(existing.getName()))
                || !normalizeSceneField(incoming.getDescription()).equals(normalizeSceneField(existing.getDescription()))
                || !normalizeSceneField(incoming.getSceneType()).equals(normalizeSceneField(existing.getSceneType()));
    }

    private String normalizeSceneField(String value) {
        return value == null ? "" : value.trim();
    }

    @Override
    public void deleteCharacter(Long id, Long tenantId) {
        Character character = characterMapper.selectById(id);
        if (character != null && tenantId.equals(character.getTenantId())) {
            characterMapper.deleteById(id);
        }
    }

    @Override
    public void deleteScene(Long id, Long tenantId) {
        Scene scene = sceneMapper.selectById(id);
        if (scene != null && tenantId.equals(scene.getTenantId())) {
            sceneMapper.deleteById(scene);
        }
    }

    @Override
    public Character uploadCharacterImage(Long characterId, Long tenantId, String viewType,
                                          InputStream stream, long size, String contentType, String ext) {
        Character character = requireCharacter(characterId, tenantId);
        String normalizedViewType = normalizeCharacterViewType(viewType);
        String safeExt = (ext == null || ext.isBlank()) ? "jpg" : ext;
        String key = "projects/" + character.getProjectId() + "/characters/" + characterId + "/" + normalizedViewType + "." + safeExt;
        tosService.upload(key, stream, size, contentType);
        applyCharacterView(character, normalizedViewType, key);
        character.setStatus("ready");
        characterMapper.updateById(character);
        return character;
    }

    @Override
    public Scene uploadSceneImage(Long sceneId, Long tenantId,
                                  InputStream stream, long size, String contentType, String ext) {
        Scene scene = sceneMapper.selectById(sceneId);
        if (scene == null || !tenantId.equals(scene.getTenantId())) {
            throw new RuntimeException("场景不存在或无权限");
        }
        String safeExt = (ext == null || ext.isBlank()) ? "jpg" : ext;
        String key = "projects/" + scene.getProjectId() + "/scenes/" + sceneId + "/image." + safeExt;
        tosService.upload(key, stream, size, contentType);
        scene.setImageTosKey(key);
        scene.setImageUrl(tosService.publicUrl(key));
        scene.setStatus("ready");
        sceneMapper.updateById(scene);
        return scene;
    }

    private Character requireCharacter(Long characterId, Long tenantId) {
        Character character = characterMapper.selectById(characterId);
        if (character == null || !tenantId.equals(character.getTenantId())) {
            throw new BizException(ErrorCode.NOT_FOUND, "角色不存在");
        }
        return character;
    }

    private void applyCharacterView(Character character, String viewType, String key) {
        String publicUrl = tosService.publicUrl(key);
        switch (normalizeCharacterViewType(viewType)) {
            case "front" -> {
                character.setImageUrl(publicUrl);
                character.setImageTosKey(key);
                character.setFrontImageUrl(publicUrl);
                character.setFrontImageTosKey(key);
            }
            case "side" -> {
                character.setSideImageUrl(publicUrl);
                character.setSideImageTosKey(key);
            }
            case "back" -> {
                character.setBackImageUrl(publicUrl);
                character.setBackImageTosKey(key);
            }
            default -> throw new BizException(ErrorCode.INVALID_PARAM, "不支持的角色视图: " + viewType);
        }
    }

    private String buildCharacterSheetPrompt(String basePrompt, ProjectVisualStyle visualStyle) {
        List<String> promptParts = new ArrayList<>();
        String normalizedPrompt = basePrompt == null ? "" : basePrompt.trim();
        if (!normalizedPrompt.isBlank()) {
            promptParts.add(normalizedPrompt);
        }
        promptParts.add(visualStyle.renderGuidance());
        promptParts.addAll(buildStyleSpecificPromptParts(visualStyle));

        // 单行四联布局：从左到右依次为大头像、正面全身、侧面全身、背面全身
        promptParts.add("character design sheet, single composite image arranged as one horizontal row of four panels");
        promptParts.add("landscape orientation canvas, aspect ratio 16:9");
        promptParts.add(
                "the canvas is divided into exactly four equal-width vertical panels of identical height and identical width, placed side by side in a single row, " +
                "all four panels share the exact same dimensions and aspect ratio, " +
                "from left to right: " +
                "panel 1: a bust portrait of the character (head and shoulders only) centered within the panel, looking straight at the camera; " +
                "panel 2: full-body front view facing camera, complete from head to toe including shoes and feet; " +
                "panel 3: full-body left side profile view (90 degrees rotation), complete from head to toe including shoes and feet; " +
                "panel 4: full-body back view facing away from camera, complete from head to toe including shoes and feet"
        );
        promptParts.add(
                "the three full-body views (panels 2, 3, 4) show the character standing naturally and relaxed, " +
                "arms hanging straight down along the sides of the body, feet together flat on the ground with both shoes fully visible, neutral expression, " +
                "no extra gestures or poses, no T-pose, no arms raised or outstretched, " +
                "the entire body must fit inside each panel with comfortable headroom above the hair and clear margin below the feet, " +
                "do not crop or cut off the feet, shoes, ankles or top of the head"
        );
        promptParts.add(
                "background: pure solid white (#FFFFFF) studio backdrop across the entire canvas and across all four panels, " +
                "flat seamless white, no gradient, no off-white tint, no cream, no beige, no gray, no colored cast, " +
                "no environment, no scenery, no props, no floor pattern, no textures, no patterns, no shadows on the background, " +
                "soft even studio lighting with no cast shadows behind the character"
        );
        promptParts.add("the four views render the exact same character: identical face, hairstyle, skin tone, body proportions, costume, accessories and footwear");
        promptParts.add("balanced composition, each figure vertically centered within its panel, full-body figures share a consistent ground line and identical scale");
        promptParts.add("no panel borders, no dividing lines, no text labels, no captions, no watermarks, no logos");
        promptParts.add("single character only, no extra people, no duplicated faces beyond these four views, no props blocking the body");
        promptParts.addAll(buildStyleNegativePromptParts(visualStyle));
        return String.join(", ", promptParts);
    }

    private String normalizeCharacterViewType(String viewType) {
        if (viewType == null || viewType.isBlank()) {
            return "front";
        }
        String normalized = viewType.toLowerCase(Locale.ROOT);
        return switch (normalized) {
            case "front", "side", "back" -> normalized;
            default -> throw new BizException(ErrorCode.INVALID_PARAM, "不支持的角色视图: " + viewType);
        };
    }

    private String buildProjectContext(Long projectId, Long tenantId) {
        Outline outline = outlineMapper.findLatestByProject(projectId, tenantId);
        List<Episode> episodes = episodeMapper.findByProject(projectId, tenantId);

        StringBuilder builder = new StringBuilder();
        if (outline != null) {
            builder.append("剧名：").append(outline.getTitle()).append('\n');
            builder.append("类型：").append(outline.getGenre()).append('\n');
            builder.append("故事梗概：").append(outline.getSynopsis()).append("\n\n");
        }
        builder.append("分集摘要：\n");
        for (Episode episode : episodes) {
            builder.append("第")
                    .append(episode.getEpisodeNumber())
                    .append("集《")
                    .append(episode.getTitle())
                    .append("》：")
                    .append(episode.getSummary())
                    .append('\n');
        }
        return builder.toString();
    }

    private String buildCharacterExtractionContext(Long projectId, Long tenantId, ProjectVisualStyle visualStyle) {
        StringBuilder builder = new StringBuilder(buildProjectContext(projectId, tenantId));
        if (builder.length() > 0) {
            builder.append("\n\n");
        }
        builder.append("Character visual style requirements:\n");
        builder.append("- Selected project style: ").append(visualStyle.label()).append('\n');
        builder.append("- Style direction: ").append(visualStyle.extractionGuidance()).append('\n');
        builder.append("- The image_prompt must be suitable for generating a consistent front/side/back turnaround sheet for the same person in the selected visual style.\n");
        builder.append("- Output target: ").append(buildExtractionTargetRule(visualStyle)).append('\n');
        builder.append("- Avoid: ").append(buildExtractionAvoidRule(visualStyle)).append('\n');
        builder.append("- Additional style notes: ").append(visualStyle.extractionGuidance()).append('\n');
        return builder.toString();
    }

    private Project loadProject(Long projectId, Long tenantId) {
        return projectMapper.findActiveById(projectId, tenantId).orElse(null);
    }

    private ProjectVisualStyle resolveProjectVisualStyle(Project project) {
        if (project == null || project.getStyle() == null || project.getStyle().isBlank()) {
            return new ProjectVisualStyle(
                    "default",
                    "真人短剧视觉",
                    "grounded live-action short drama characters with realistic styling",
                    "grounded live-action short drama aesthetic, cinematic realism, realistic wardrobe"
            );
        }

        String slug = project.getStyle().trim().toLowerCase(Locale.ROOT);
        return switch (slug) {
            case "live_action_drama" -> new ProjectVisualStyle(
                    slug,
                    "真人短剧视觉",
                    "live-action short drama styling with realistic actors, believable wardrobe, and production-ready character design",
                    "live-action short drama aesthetic, realistic actors, cinematic realism, believable wardrobe details"
            );
            case "japanese_anime" -> new ProjectVisualStyle(
                    slug,
                    "2D日漫",
                    "2D Japanese anime styling with clean line art, cel shading, expressive silhouettes, and strong character readability",
                    "2D Japanese anime aesthetic, clean line art, cel shading, expressive character design, polished turnaround sheet"
            );
            case "korean_webtoon" -> new ProjectVisualStyle(
                    slug,
                    "2D韩漫都市",
                    "Korean webtoon urban styling with fashionable silhouettes, soft shading, and modern romance or city-drama appeal",
                    "2D Korean webtoon aesthetic, modern urban fashion, clean line art, soft shading, stylish character sheet"
            );
            case "chinese_anime" -> new ProjectVisualStyle(
                    slug,
                    "2D国漫",
                    "2D Chinese animation styling with strong silhouette design, refined costume details, and contemporary guoman appeal",
                    "2D Chinese animation aesthetic, clean line art, refined costume design, guoman character sheet"
            );
            case "chinese_fantasy_3d" -> new ProjectVisualStyle(
                    slug,
                    "3D国风仙侠",
                    "stylized 3D Chinese fantasy styling with layered costume details, elegant accessories, and xianxia or guofeng presentation",
                    "3D Chinese fantasy aesthetic, stylized character render, ornate costume layers, xianxia guofeng hero design"
            );
            case "cg_cinematic" -> new ProjectVisualStyle(
                    slug,
                    "CG电影感",
                    "high-end CGI cinematic styling with hero-asset presentation, premium materials, and polished production design",
                    "cinematic CGI character design, premium materials, high-end render quality, hero asset presentation"
            );
            case "urban" -> new ProjectVisualStyle(
                    slug,
                    "Urban emotional drama",
                    "modern city setting, fashionable contemporary wardrobe, restrained makeup, grounded live-action drama references",
                    "modern urban live-action drama aesthetic, contemporary Chinese wardrobe, cinematic realism, grounded emotional tone"
            );
            case "fantasy" -> new ProjectVisualStyle(
                    slug,
                    "Fantasy xianxia drama",
                    "fantasy or xianxia worldbuilding, layered costume details, elegant accessories, but still designed as live-action costume drama characters",
                    "Chinese fantasy xianxia live-action costume drama aesthetic, refined accessories, layered fabrics, cinematic realism"
            );
            case "romance" -> new ProjectVisualStyle(
                    slug,
                    "Romance drama",
                    "attractive but believable real-world styling, polished wardrobe, intimate emotional tone, live-action drama reference",
                    "contemporary live-action romance drama aesthetic, polished styling, soft cinematic lighting, realistic people"
            );
            case "historical" -> new ProjectVisualStyle(
                    slug,
                    "Historical costume drama",
                    "historical Chinese costume drama styling, era-appropriate silhouette and accessories, realistic live-action wardrobe construction",
                    "Chinese historical live-action costume drama aesthetic, period wardrobe, elegant materials, cinematic realism"
            );
            case "suspense" -> new ProjectVisualStyle(
                    slug,
                    "Suspense thriller",
                    "grounded thriller styling, practical wardrobe, realistic tension, understated colors, live-action reference",
                    "grounded suspense thriller live-action aesthetic, restrained palette, realistic wardrobe, cinematic realism"
            );
            case "comedy" -> new ProjectVisualStyle(
                    slug,
                    "Comedy drama",
                    "bright and expressive but still realistic live-action styling, contemporary wardrobe, approachable character design",
                    "bright live-action comedy-drama aesthetic, expressive styling, realistic people, clean contemporary wardrobe"
            );
            default -> new ProjectVisualStyle(
                    slug,
                    "Custom style: " + slug,
                    "match the selected project style while keeping the result visually coherent and suitable for character turnaround generation",
                    "match the selected project style `" + slug + "` with a clear, consistent character-design presentation"
            );
        };
    }

    private List<String> buildStyleSpecificPromptParts(ProjectVisualStyle visualStyle) {
        return switch (visualStyle.slug()) {
            case "japanese_anime" -> List.of(
                    "2D Japanese anime character turnaround sheet",
                    "clean line art",
                    "cel shading",
                    "anime-style color blocking"
            );
            case "korean_webtoon" -> List.of(
                    "2D Korean webtoon character turnaround sheet",
                    "clean line art",
                    "soft webtoon shading",
                    "modern urban fashion styling"
            );
            case "chinese_anime" -> List.of(
                    "2D Chinese animation character turnaround sheet",
                    "clean line art",
                    "stylized guoman character design",
                    "refined costume detailing"
            );
            case "chinese_fantasy_3d" -> List.of(
                    "3D Chinese fantasy character turnaround sheet",
                    "stylized 3D render",
                    "ornate costume layers",
                    "xianxia-inspired accessories"
            );
            case "cg_cinematic" -> List.of(
                    "cinematic CGI character turnaround sheet",
                    "high-end CGI rendering",
                    "hero asset presentation",
                    "premium material definition"
            );
            default -> List.of(
                    "live-action TV drama character turnaround sheet",
                    "production-ready costume design reference",
                    "realistic human anatomy and natural skin texture"
            );
        };
    }

    private List<String> buildStyleNegativePromptParts(ProjectVisualStyle visualStyle) {
        return switch (visualStyle.slug()) {
            case "japanese_anime", "korean_webtoon", "chinese_anime" -> List.of(
                    "not live action",
                    "not photorealistic skin texture",
                    "not 3D render",
                    "not photo"
            );
            case "chinese_fantasy_3d", "cg_cinematic" -> List.of(
                    "not live action photo",
                    "not flat 2D illustration",
                    "not manga panel",
                    "not collage"
            );
            default -> List.of(
                    "not anime",
                    "not manga",
                    "not cartoon",
                    "not cel shaded",
                    "not illustration"
            );
        };
    }

    private String buildExtractionTargetRule(ProjectVisualStyle visualStyle) {
        return switch (visualStyle.slug()) {
            case "japanese_anime" -> "a 2D Japanese anime turnaround sheet with front, side, and back consistency";
            case "korean_webtoon" -> "a 2D Korean webtoon turnaround sheet with modern urban styling and clear silhouette consistency";
            case "chinese_anime" -> "a 2D guoman turnaround sheet with refined costume details and consistent proportions";
            case "chinese_fantasy_3d" -> "a 3D Chinese fantasy turnaround sheet with layered costume construction and clean silhouette readability";
            case "cg_cinematic" -> "a cinematic CGI turnaround sheet with premium material definition and hero-asset clarity";
            default -> "a live-action short drama turnaround sheet with realistic wardrobe, hair, and body proportions";
        };
    }

    private String buildExtractionAvoidRule(ProjectVisualStyle visualStyle) {
        return switch (visualStyle.slug()) {
            case "japanese_anime", "korean_webtoon", "chinese_anime" ->
                    "live-action photography, photorealistic actor language, or realistic skin-texture wording";
            case "chinese_fantasy_3d", "cg_cinematic" ->
                    "flat 2D illustration wording, manga panel composition, or live-action actor-photo language";
            default ->
                    "anime, manga, cartoon, cel-shaded, or illustrated wording unless the selected style explicitly requires it";
        };
    }

    private byte[] downloadBytes(String url) throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .timeout(Duration.ofMinutes(2))
                .GET()
                .build();
        HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
        if (response.statusCode() != 200) {
            throw new BizException(ErrorCode.INTERNAL_ERROR, "下载图片失败 HTTP " + response.statusCode());
        }
        return response.body();
    }

    private String extractJson(String raw) {
        String text = raw == null ? "" : raw.strip();
        if (text.startsWith("```")) {
            int start = text.indexOf('\n') + 1;
            int end = text.lastIndexOf("```");
            if (end > start) {
                text = text.substring(start, end).strip();
            }
        }
        return text;
    }

    private record ProjectVisualStyle(
            String slug,
            String label,
            String extractionGuidance,
            String renderGuidance
    ) {
    }
}
