# TeamTranslationService.java

    package com.yumu.noveltranslator.adapter.out.translate;

    import com.yumu.noveltranslator.port.dto.translation.TeamTranslateRequest;
    import com.yumu.noveltranslator.port.dto.translation.TeamTranslateResponse;
    import com.yumu.noveltranslator.domain.model.Glossary;
    import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.http.MediaType;
    import org.springframework.stereotype.Service;
    import org.springframework.web.reactive.function.client.WebClient;
    import org.springframework.web.reactive.function.client.WebClientResponseException;

    import java.time.Duration;
    import java.util.Comparator;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;

    /**
     * AI 翻译团队服务
     * 调用 Python 侧的 /translate-team 端点，使用 AI 多角色协作进行小说章节翻译
     */
    @Service
    @Slf4j
    public class TeamTranslationService implements com.yumu.noveltranslator.port.out.TeamTranslationPort {

        private final WebClient webClient;

        public TeamTranslationService(
                @Value("${translate-server.host:localhost}") String host,
                @Value("${translate-server.port:8000}") int port,
                @Value("${translate-server.api-key:}") String apiKey) {
            var builder = WebClient.builder()
                    .baseUrl("http://%s:%d".formatted(host, port))
                    .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
                    .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(512 * 1024));
            if (apiKey != null && !apiKey.isEmpty()) {
                builder.defaultHeader("X-Service-Key", apiKey);
            }
            this.webClient = builder.build();
        }

        /**
         * 调用 AI 翻译团队进行章节翻译
         *
         * @param text         待翻译的章节原文
         * @param novelType    小说类型
         * @param sourceLang   源语言
         * @param targetLang   目标语言
         * @param glossaryTerms 术语表词条列表
         * @return 翻译后的文本
         */
        @CircuitBreaker(name = "teamTranslation", fallbackMethod = "teamTranslationFallback")
        public String translateChapter(String text, String novelType, String sourceLang,
                                       String targetLang, List<Glossary> glossaryTerms) {

            TeamTranslateRequest request = buildRequest(text, novelType, sourceLang, targetLang, glossaryTerms);

            log.info("调用 AI 翻译团队: sourceLang={}, targetLang={}, novelType={}, textLength={}, glossaryCount={}",
                    sourceLang, targetLang, novelType, text != null ? text.length() : 0,
                    glossaryTerms != null ? glossaryTerms.size() : 0);

            try {
                String responseBody = webClient.post()
                        .uri("/translate-team")
                        .bodyValue(request)
                        .retrieve()
                        .bodyToMono(String.class)
                        .block(Duration.ofSeconds(300));

                TeamTranslateResponse response = parseResponse(responseBody);

                if (response.getCode() != 200) {
                    log.error("AI 翻译团队返回错误码: code={}", response.getCode());
                    throw new RuntimeException("AI 翻译团队翻译失败，错误码: " + response.getCode());
                }

                log.info("AI 翻译团队调用成功: chunkCount={}, costMs={}ms",
                        response.getChunkCount(), response.getCostMs());

                return response.getData();

            } catch (WebClientResponseException e) {
                log.error("AI 翻译团队调用失败，状态码：{}，响应体：{}", e.getStatusCode(), e.getResponseBodyAsString(), e);
                throw new RuntimeException("AI 翻译团队调用失败 (HTTP " + e.getStatusCode() + "): " + e.getMessage(), e);
            } catch (Exception e) {
                String errorMsg = e.getMessage() != null ? e.getMessage() : "未知错误";
                if (errorMsg.contains("Timeout") || errorMsg.contains("timeout") || errorMsg.contains("timed out")) {
                    log.error("AI 翻译团队响应超时（>300 秒），文本长度：{}", text != null ? text.length() : 0, e);
                    throw new RuntimeException("AI 翻译团队响应超时：" + e.getMessage(), e);
                }
                if (e.getCause() instanceof java.net.ConnectException
                        || errorMsg.contains("Connection refused")
                        || errorMsg.contains("connect timed out")) {
                    log.error("AI 翻译团队连接失败，请确认服务已启动在 http://localhost:{}，错误：{}",
                            8000, e.getMessage(), e);
                    throw new RuntimeException("无法连接到 AI 翻译团队服务：" + e.getMessage(), e);
                }
                log.error("AI 翻译团队翻译异常，文本长度：{}", text != null ? text.length() : 0, e);
                throw new RuntimeException("AI 翻译团队翻译失败：" + e.getMessage(), e);
            }
        }

        /**
         * 调用 AI 翻译团队进行章节翻译（带占位符保护）
         *
         * @param text              待翻译的章节原文（可能包含 [{1}], [{2}] 等占位符）
         * @param novelType         小说类型
         * @param sourceLang        源语言
         * @param targetLang        目标语言
         * @param glossaryTerms     术语表词条列表
         * @param placeholderMap    占位符 → 翻译后实体名的映射
         * @return 翻译后的文本（占位符已还原为翻译后的实体名）
         */
        @CircuitBreaker(name = "teamTranslation", fallbackMethod = "teamTranslationFallback")
        public String translateChapterWithPlaceholders(String text, String novelType, String sourceLang,
                                                       String targetLang, List<Glossary> glossaryTerms,
                                                       Map<String, String> placeholderMap) {

            TeamTranslateRequest request = buildRequest(text, novelType, sourceLang, targetLang, glossaryTerms);

            // Attach placeholders to the request
            if (placeholderMap != null && !placeholderMap.isEmpty()) {
                request.setPlaceholders(placeholderMap);
                log.info("占位符保护: 发送 {} 个占位符映射", placeholderMap.size());
            } else {
                log.info("占位符保护: 无占位符映射");
            }

            log.info("调用 AI 翻译团队(带占位符): sourceLang={}, targetLang={}, novelType={}, textLength={}, glossaryCount={}, placeholderCount={}",
                    sourceLang, targetLang, novelType, text != null ? text.length() : 0,
                    glossaryTerms != null ? glossaryTerms.size() : 0,
                    placeholderMap != null ? placeholderMap.size() : 0);

            try {
                String responseBody = webClient.post()
                        .uri("/translate-team")
                        .bodyValue(request)
                        .retrieve()
                        .bodyToMono(String.class)
                        .block(Duration.ofSeconds(300));

                TeamTranslateResponse response = parseResponse(responseBody);

                if (response.getCode() != 200) {
                    log.error("AI 翻译团队返回错误码: code={}", response.getCode());
                    throw new RuntimeException("AI 翻译团队翻译失败，错误码: " + response.getCode());
                }

                log.info("AI 翻译团队调用成功(带占位符): chunkCount={}, costMs={}ms",
                        response.getChunkCount(), response.getCostMs());

                String translatedText = response.getData();

                // Restore placeholders in the translated text
                if (placeholderMap != null && !placeholderMap.isEmpty()) {
                    translatedText = restorePlaceholdersInText(translatedText, placeholderMap);
                }

                return translatedText;

            } catch (WebClientResponseException e) {
                log.error("AI 翻译团队调用失败，状态码：{}，响应体：{}", e.getStatusCode(), e.getResponseBodyAsString(), e);
                throw new RuntimeException("AI 翻译团队调用失败 (HTTP " + e.getStatusCode() + "): " + e.getMessage(), e);
            } catch (Exception e) {
                String errorMsg = e.getMessage() != null ? e.getMessage() : "未知错误";
                if (errorMsg.contains("Timeout") || errorMsg.contains("timeout") || errorMsg.contains("timed out")) {
                    log.error("AI 翻译团队响应超时（>300 秒），文本长度：{}", text != null ? text.length() : 0, e);
                    throw new RuntimeException("AI 翻译团队响应超时：" + e.getMessage(), e);
                }
                if (e.getCause() instanceof java.net.ConnectException
                        || errorMsg.contains("Connection refused")
                        || errorMsg.contains("connect timed out")) {
                    log.error("AI 翻译团队连接失败，请确认服务已启动在 http://localhost:{}，错误：{}",
                            8000, e.getMessage(), e);
                    throw new RuntimeException("无法连接到 AI 翻译团队服务：" + e.getMessage(), e);
                }
                log.error("AI 翻译团队翻译异常，文本长度：{}", text != null ? text.length() : 0, e);
                throw new RuntimeException("AI 翻译团队翻译失败：" + e.getMessage(), e);
            }
        }

        /**
         * 在翻译后的文本中还原占位符
         * 按占位符字符串长度降序替换，避免 [{1}] 匹配到 [{10}] 中的部分子串
         * 如果某个占位符在响应中未找到，会记录警告日志
         *
         * @param text           翻译后的文本
         * @param placeholderMap 占位符 → 翻译后实体名的映射
         * @return 还原占位符后的文本
         */
        private String restorePlaceholdersInText(String text, Map<String, String> placeholderMap) {
            if (text == null || text.isEmpty() || placeholderMap == null || placeholderMap.isEmpty()) {
                return text;
            }

            String result = text;

            // Sort placeholders by key length descending to avoid partial matches
            List<Map.Entry<String, String>> sortedEntries = new HashMap<>(placeholderMap).entrySet().stream()
                    .sorted(Map.Entry.<String, String>comparingByKey(Comparator.comparingInt(String::length).reversed()))
                    .collect(Collectors.toList());

            int restoredCount = 0;
            int notFoundCount = 0;

            for (Map.Entry<String, String> entry : sortedEntries) {
                String placeholder = entry.getKey();
                String translatedEntity = entry.getValue();

                if (result.contains(placeholder)) {
                    result = result.replace(placeholder, translatedEntity);
                    restoredCount++;
                    log.debug("占位符还原: '{}' -> '{}'", placeholder, translatedEntity);
                } else {
                    notFoundCount++;
                    log.warn("占位符还原: 响应中未找到占位符 '{}' (映射到 '{}')，AI 可能未正确翻译该实体",
                            placeholder, translatedEntity);
                }
            }

            log.info("占位符还原完成: 总数={}, 已还原={}, 未找到={}",
                    sortedEntries.size(), restoredCount, notFoundCount);

            return result;
        }

        /**
         * 将请求参数构建为 TeamTranslateRequest
         */
        private TeamTranslateRequest buildRequest(String text, String novelType, String sourceLang,
                                                  String targetLang, List<Glossary> glossaryTerms) {
            TeamTranslateRequest request = new TeamTranslateRequest();
            request.setText(text);
            request.setNovelType(novelType);
            request.setSourceLang(sourceLang);
            request.setTargetLang(targetLang);

            if (glossaryTerms != null && !glossaryTerms.isEmpty()) {
                List<TeamTranslateRequest.GlossaryTerm> terms = glossaryTerms.stream()
                        .map(g -> new TeamTranslateRequest.GlossaryTerm(
                                g.getSourceWord(),
                                g.getTargetWord(),
                                g.getRemark()))
                        .collect(Collectors.toList());
                request.setGlossaryTerms(terms);
            }

            return request;
        }

        /**
         * 解析响应 JSON 为 TeamTranslateResponse
         */
        private TeamTranslateResponse parseResponse(String responseBody) {
            try {
                return com.alibaba.fastjson2.JSON.parseObject(responseBody, TeamTranslateResponse.class);
            } catch (Exception e) {
                log.error("解析 AI 翻译团队响应失败: {}", responseBody, e);
                TeamTranslateResponse fallback = new TeamTranslateResponse();
                fallback.setCode(-1);
                fallback.setData(responseBody);
                return fallback;
            }
        }

        private String teamTranslationFallback(String text, String novelType, String sourceLang,
                                               String targetLang, List<Glossary> glossaryTerms, Throwable t) {
            log.error("AI 翻译团队熔断降级: {}", t.getMessage());
            throw new RuntimeException("AI 翻译团队服务不可用（熔断器已打开）: " + t.getMessage(), t);
        }

        private String teamTranslationFallback(String text, String novelType, String sourceLang,
                                               String targetLang, List<Glossary> glossaryTerms,
                                               Map<String, String> placeholderMap, Throwable t) {
            log.error("AI 翻译团队熔断降级(带占位符): {}", t.getMessage());
            throw new RuntimeException("AI 翻译团队服务不可用（熔断器已打开）: " + t.getMessage(), t);
        }
    }