package dev.awesomedev.mirim.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

import java.time.Duration;
import java.util.List;
import java.util.Map;

/**
 * 이 파일이 하는 일:
 * 로컬 LLM(Ollama)에게 "학생 코드에 대한 멘토 피드백 초안"을 받아 온다.
 * 최종 피드백은 멘토가 쓴다 — 이건 백지 대신 고칠 초안을 줄 뿐이다.
 *
 * 학습 포인트 ① — LLM은 "판정자"가 아니라 "초안 작성자"다.
 *   이 앱의 CodingService 주석에 이미 있다: "진짜 판정은 멘토가 코드 원문을 읽고 내린다".
 *   그 원칙을 그대로 지킨다. LLM 출력을 그대로 저장하지 않는다 — 멘토 화면에 초안으로
 *   띄우고, 멘토가 고쳐서 기존 피드백 저장 API로 보낸다. 왜 이렇게까지 하냐면,
 *   로컬 모델은 정상 코드에도 없는 버그를 지어낼 때가 있기 때문이다(측정으로 확인됨).
 *   그래서 "빠른 초안 + 사람 검수"가 유일하게 안전한 사용법이다.
 *
 * 학습 포인트 ② — 부가 기능이 본 기능을 죽이면 안 된다 (TelegramNotifier와 같은 원칙).
 *   초안 생성이 실패하거나 모델이 없어도, 멘토는 늘 하던 대로 손으로 피드백을 쓸 수 있어야
 *   한다. 그래서 예외를 밖으로 던지지 않고 null을 돌려준다 — 호출한 쪽은 "초안 없음"으로
 *   처리하면 된다. 모델 URL이 비어 있으면 아예 호출도 안 한다(로컬에 모델이 없어도 앱이 돈다).
 *
 * 학습 포인트 ③ — 왜 하필 이 설정인가 (E:\llm 평가 결과에서 가져온 값).
 *   · /api/chat 을 쓴다. /api/generate 로 부르면 이 모델(gpt-oss)이 추론만 하다 답을 못 낸다.
 *   · num_ctx 16384. 작으면 추론에 예산을 다 쓰고 답변이 빈다.
 *   · 모델은 gpt-oss:20b. 버그 설명 정확도가 로컬 모델 중 유일하게 쓸 만했다(6/6).
 *     빠른 qwen 계열은 async 버그를 "정상"이라 놓쳐서 피드백에는 위험하다.
 */
@Service
public class LlmFeedbackDrafter {

    private static final Logger log = LoggerFactory.getLogger(LlmFeedbackDrafter.class);

    private final String baseUrl;
    private final String model;
    private final int numCtx;
    private final RestClient restClient;

    public LlmFeedbackDrafter(
            @Value("${llm.base-url:}") String baseUrl,
            @Value("${llm.model:gpt-oss:20b}") String model,
            @Value("${llm.num-ctx:16384}") int numCtx) {
        this.baseUrl = baseUrl;
        this.model = model;
        this.numCtx = numCtx;
        // 로컬 모델은 답변 전에 추론을 길게 한다 — 읽기 타임아웃을 넉넉히(5분) 잡는다.
        // 연결 자체는 같은 머신이라 금방 된다.
        this.restClient = RestClient.builder()
                .baseUrl(baseUrl.isBlank() ? "http://127.0.0.1:11434" : baseUrl)
                .requestFactory(timeouts(Duration.ofSeconds(5), Duration.ofMinutes(5)))
                .build();
        if (!isEnabled()) {
            log.info("LLM 피드백 초안 비활성 상태입니다 (llm.base-url 미설정).");
        }
    }

    private static org.springframework.http.client.ClientHttpRequestFactory timeouts(
            Duration connect, Duration read) {
        var f = new org.springframework.http.client.SimpleClientHttpRequestFactory();
        f.setConnectTimeout((int) connect.toMillis());
        f.setReadTimeout((int) read.toMillis());
        return f;
    }

    private boolean isEnabled() {
        return !baseUrl.isBlank();
    }

    /**
     * 학생 제출 코드에 대한 피드백 초안을 만든다. 실패하거나 비활성이면 null.
     *
     * @param language    문제 언어 (JavaScript / SQL / Java)
     * @param title       문제 제목
     * @param description 문제 설명
     * @param studentCode 학생이 낸 코드 원문
     * @return 피드백 초안 텍스트, 또는 null(멘토가 손으로 쓰면 된다)
     */
    public String draft(String language, String title, String description, String studentCode) {
        if (!isEnabled() || studentCode == null || studentCode.isBlank()) {
            return null;
        }
        String prompt = buildPrompt(language, title, description, studentCode);
        try {
            // Ollama /api/chat 를 non-stream 으로 부른다. 응답은 message.content 에 온다
            // (추론은 message.thinking 으로 따로 오지만 피드백에는 쓰지 않는다).
            @SuppressWarnings("unchecked")
            Map<String, Object> body = restClient.post()
                    .uri("/api/chat")
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(Map.of(
                            "model", model,
                            "stream", false,
                            "messages", List.of(Map.of("role", "user", "content", prompt)),
                            "options", Map.of("num_ctx", numCtx, "temperature", 0)))
                    .retrieve()
                    .body(Map.class);

            if (body == null) return null;
            Object message = body.get("message");
            if (!(message instanceof Map<?, ?> m)) return null;
            Object content = m.get("content");
            String text = content == null ? "" : content.toString().trim();
            return text.isBlank() ? null : text;
        } catch (Exception e) {
            // 실패는 조용히 삼킨다 — 멘토는 늘 하던 대로 손으로 쓰면 된다.
            log.warn("LLM 피드백 초안 생성 실패(무시하고 계속): {}", e.getClass().getSimpleName());
            return null;
        }
    }

    private String buildPrompt(String language, String title, String description, String studentCode) {
        // 실측 근거: gpt-oss 는 "정직하게 보고 정상이면 정상이라 하라"는 지시를 지켜,
        // 정상 코드에 없는 버그를 지어내지 않았다(환각저항 3/3). 그 지시를 그대로 넣는다.
        return """
                너는 프로그래밍 멘토의 보조다. 아래는 학생이 낸 %s 코드다.
                멘토가 검토하고 다듬을 **피드백 초안**을 한국어로 써라. 다음을 지켜라.

                - 정답 코드를 통째로 주지 마라. 학생이 스스로 고치도록 방향만 짚어라.
                - 코드를 정직하게 봐라. 정말 문제가 없으면 없는 버그를 지어내지 말고 잘한 점을 말하라.
                - 문제가 있으면 무엇이 왜 문제인지, 어디를 다시 보면 되는지 구체적으로.
                - 말투는 학생을 격려하되 정확하게. 3~5문장.

                문제: [%s] %s
                %s

                학생 코드:
                ```
                %s
                ```
                """.formatted(language, language, title,
                description == null ? "" : description, studentCode);
    }
}
