package com.zioinfo.hrm.common.ai;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zioinfo.hrm.common.ai.TextAiClient.GenResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

/**
 * 외부 Claude(Anthropic) Messages API 텍스트 생성 클라이언트. [ISSUER=GUARDiA-HRM]
 *
 * <p><b>외부 호출 예외 허용</b>: 본 클라이언트는 소유자 승인(폐쇄망 아님)에 따라 {@code api.anthropic.com}
 * 호출이 허용된 유일한 외부 경로다. 그 외 외부 API 호출은 여전히 금지(Ollama localhost 전용).
 *
 * <p><b>API 키 보안(최우선)</b>: 키는 환경변수 {@code ANTHROPIC_API_KEY} 에서만 로드하며
 * ({@code @Value("${ANTHROPIC_API_KEY:}")}), DB·코드·커밋·로그·응답·예외 메시지 어디에도 기록하지 않는다.
 * 키는 오직 HTTP 헤더 {@code x-api-key} 로만 전달된다. 로그는 상태코드/예외 클래스명만 남긴다(본문·키 미기록).
 *
 * <p><b>실패 처리</b>: 키 미설정/타임아웃/비200/예외 시 {@code GenResult.degraded=true·text=null} 반환
 * (예외를 던지지 않음 → 호출자는 Ollama 폴백). 신규 라이브러리 0 — JDK {@link HttpClient} 사용.
 *
 * <p>모델은 인자로 받는다(호출자가 화이트리스트 검증된 모델 ID 전달). 인터페이스 {@link TextAiClient}의
 * {@code generate(String)} 은 기본 모델로 위임한다(주로 라우터가 모델을 명시 호출).
 *
 * <p><b>레퍼런스</b>: guardia-ocr {@code com.zioinfo.ocr.common.ai.ClaudeTextClient} 미러(패키지·ISSUER 치환).
 */
@Slf4j
@Service
public class ClaudeTextClient implements TextAiClient {

    /** Anthropic Messages API 엔드포인트(외부 호출 예외 허용 단일 경로). */
    private static final String API_URL = "https://api.anthropic.com/v1/messages";
    /** Anthropic 버전 헤더(고정). */
    private static final String ANTHROPIC_VERSION = "2023-06-01";
    /** 기본 모델(키 검증된 화이트리스트의 기본값). */
    static final String DEFAULT_MODEL = "claude-sonnet-4-6";
    /** 일반 생성 max_tokens. */
    private static final int DEFAULT_MAX_TOKENS = 1024;
    /** 콜드 응답 대비 요청 타임아웃(초). Ollama timeout 과 동급. */
    private static final int TIMEOUT_SEC = 120;
    private static final int CONNECT_TIMEOUT_SEC = 15;

    private static final ObjectMapper MAPPER = new ObjectMapper();

    /** 환경변수 전용 주입. 미설정 시 빈 문자열(=미구성). 값은 절대 외부 노출/로그 금지. */
    @Value("${ANTHROPIC_API_KEY:}")
    private String apiKey;

    /** API 키가 환경변수로 설정되어 있는지(여부만 — 값/길이/마스킹 일절 미노출). */
    public boolean isConfigured() {
        return apiKey != null && !apiKey.isBlank();
    }

    /** {@link TextAiClient} 계약: 기본 모델로 생성. */
    @Override
    public GenResult generate(String prompt) {
        return generate(prompt, DEFAULT_MODEL);
    }

    /** 지정 모델로 생성(라우터가 화이트리스트 검증된 모델 ID 전달). */
    public GenResult generate(String prompt, String model) {
        return generate(prompt, model, DEFAULT_MAX_TOKENS);
    }

    /**
     * Anthropic Messages API 호출. 실패 시 {@code degraded=true·text=null}.
     * 키·요청본문·응답본문은 로그에 남기지 않는다(상태코드/예외 클래스명만).
     */
    public GenResult generate(String prompt, String model, int maxTokens) {
        if (!isConfigured()) {
            return new GenResult(null, true);
        }
        if (prompt == null || prompt.isBlank()) {
            return new GenResult(null, true);
        }
        String useModel = (model == null || model.isBlank()) ? DEFAULT_MODEL : model.trim();
        int tokens = maxTokens > 0 ? maxTokens : DEFAULT_MAX_TOKENS;
        try {
            String payload = "{"
                    + "\"model\":" + str(useModel) + ","
                    + "\"max_tokens\":" + tokens + ","
                    + "\"messages\":[{\"role\":\"user\",\"content\":" + str(prompt) + "}]"
                    + "}";

            HttpClient client = HttpClient.newBuilder()
                    .connectTimeout(Duration.ofSeconds(CONNECT_TIMEOUT_SEC))
                    .build();
            HttpRequest req = HttpRequest.newBuilder()
                    .uri(URI.create(API_URL))
                    .timeout(Duration.ofSeconds(TIMEOUT_SEC))
                    .header("content-type", "application/json")
                    .header("x-api-key", apiKey)              // 키는 헤더로만 — 로그/응답 미노출
                    .header("anthropic-version", ANTHROPIC_VERSION)
                    .POST(HttpRequest.BodyPublishers.ofString(payload))
                    .build();
            HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
            if (resp.statusCode() != 200) {
                log.warn("Claude API status {} -> degraded fallback", resp.statusCode());  // 본문 미기록
                return new GenResult(null, true);
            }
            String text = extractText(resp.body());
            if (text == null || text.isBlank()) {
                return new GenResult(null, true);
            }
            return new GenResult(text.trim(), false);
        } catch (Exception e) {
            log.warn("Claude API call failed -> degraded fallback: {}", e.getClass().getSimpleName());  // 메시지·키 미기록
            return new GenResult(null, true);
        }
    }

    /** Messages API 응답에서 content[].text(type=text) 추출·연결. */
    private static String extractText(String body) {
        if (body == null || body.isBlank()) {
            return null;
        }
        try {
            JsonNode root = MAPPER.readTree(body);
            JsonNode content = root.get("content");
            if (content == null || !content.isArray()) {
                return null;
            }
            StringBuilder sb = new StringBuilder();
            for (JsonNode block : content) {
                JsonNode type = block.get("type");
                if (type != null && "text".equals(type.asText())) {
                    JsonNode t = block.get("text");
                    if (t != null && !t.isNull()) {
                        sb.append(t.asText());
                    }
                }
            }
            String out = sb.toString();
            return out.isBlank() ? null : out;
        } catch (Exception e) {
            return null;
        }
    }

    /** JSON 문자열 이스케이프. */
    private static String str(String s) {
        if (s == null) {
            return "\"\"";
        }
        StringBuilder sb = new StringBuilder("\"");
        for (char c : s.toCharArray()) {
            switch (c) {
                case '"' -> sb.append("\\\"");
                case '\\' -> sb.append("\\\\");
                case '\n' -> sb.append("\\n");
                case '\r' -> sb.append("\\r");
                case '\t' -> sb.append("\\t");
                default -> {
                    if (c < 0x20) {
                        sb.append(String.format("\\u%04x", (int) c));
                    } else {
                        sb.append(c);
                    }
                }
            }
        }
        return sb.append("\"").toString();
    }
}
