package com.seer.teach.teacher.service.impl;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.seer.teach.common.config.LlmConfig;
import com.seer.teach.common.enums.ResultCodeEnum;
import com.seer.teach.common.exception.CommonException;
import com.seer.teach.common.exception.RequestLlmFailException;
import com.seer.teach.teacher.common.llm.req.JsonRpcReq;
import com.seer.teach.teacher.common.llm.req.LlmGetPlanQuestionReq;
import com.seer.teach.teacher.common.llm.req.QuestionCorrectLlmReq;
import com.seer.teach.teacher.common.llm.req.QuestionsCorrectLlmReq;
import com.seer.teach.teacher.common.llm.resp.*;
import com.seer.teach.teacher.common.req.QuestionCorrectReq;
import com.seer.teach.teacher.common.resp.SubjectScoreResp;
import com.seer.teach.teacher.common.utils.TeacherUtils;
import com.seer.teach.teacher.convert.LlmConvert;
import com.seer.teach.teacher.convert.QuestionConvert;
import com.seer.teach.teacher.module.entity.BankQuestionsEntity;
import com.seer.teach.teacher.service.BankQuestionsService;
import com.seer.teach.teacher.service.LlmRequestLogsService;
import com.seer.teach.teacher.service.LlmRequestService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * LLM请求服务实现类，提供与大语言模型交互的相关功能。
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class LlmRequestServiceImpl implements LlmRequestService {

    private final RestTemplate restTemplate;

    private final BankQuestionsService bankQuestionsService;

    private final LlmRequestLogsService llmRequestLogsService;

    /**
     * 执行通用的LLM请求，并处理日志记录、异常捕获等逻辑。
     *
     * @param userId     用户ID，用于标识当前操作用户
     * @param action     接口名称或动作标识，如"questionCorrect"
     * @param req        请求对象，封装了JSON-RPC格式的数据
     * @param resultType 返回结果的具体类型（泛型）
     * @return 解析后的响应数据对象
     */
    private <T> T executeRequest(Integer userId, String action, JsonRpcReq req, Class<T> resultType) {
        Integer sessionId = TeacherUtils.generateSessionId(req);
        llmRequestLogsService.saveRequestLog(userId, action, sessionId, req, req);

        try {
            String body = doPostAndRequireBody(req, action + "接口调用失败");
            JSONObject json = parseJsonOrThrow(body, action + "接口调用失败");

            Object result = requireResult(json, action + "接口返回result为空");
            T resp = parseResponse(result, resultType);

            llmRequestLogsService.updateResponseLog(sessionId, resp);
            return resp;
        } catch (Exception e) {
            handleError(sessionId, e);
            throw wrapException(action, e);
        }
    }

    /**
     * 执行通用的LLM数组类型请求并解析为指定元素类型的列表。
     *
     * @param userId      用户ID，用于标识当前操作用户
     * @param action      接口名称或动作标识，如"getPlanQuestions"
     * @param req         请求对象，封装了JSON-RPC格式的数据
     * @param resultType  响应结果的整体类型（通常是List.class）
     * @param elementType 列表中每个元素的类型
     * @return 解析后的响应数据对象列表
     */
    private <T> T executeRequest(Integer userId, String action, JsonRpcReq req,
                                 Class<T> resultType, Class<?> elementType) {

        Integer sessionId = TeacherUtils.generateSessionId(req);
        llmRequestLogsService.saveRequestLog(userId, action, sessionId, req, req);

        try {
            String body = doPostAndRequireBody(req, action + "接口调用失败");
            JSONObject json = parseJsonOrThrow(body, action + "接口调用失败");

            Object result = requireResult(json, action + "接口返回result为空");
            T resp = (T) JSON.parseArray(String.valueOf(result), elementType);

            llmRequestLogsService.updateResponseLog(sessionId, resp);
            return resp;
        } catch (Exception e) {
            handleError(sessionId, e);
            throw wrapException(action, e);
        }
    }

    /**
     * 处理请求过程中发生的异常，并更新日志记录中的错误信息。
     *
     * @param sessionId 当前会话ID，用于定位具体请求日志
     * @param e         异常对象
     */
    private void handleError(Integer sessionId, Exception e) {
        if (sessionId != null) {
            llmRequestLogsService.updateEmptyResponseLog(sessionId);
            llmRequestLogsService.updateErrorLog(sessionId, e.getMessage());
        }
    }

    /**
     * 将原始异常包装成统一的业务异常抛出。
     *
     * @param action 操作名称，用于构建异常消息
     * @param e      原始异常对象
     * @return 包装后的运行时异常
     */
    private RuntimeException wrapException(String action, Exception e) {
        return (e instanceof CommonException)
                ? (CommonException) e
                : new RequestLlmFailException(action + "接口调用失败", e);
    }

    /**
     * 根据给定的结果类型将JSON结果转换为目标对象。
     *
     * @param result     原始JSON结果对象
     * @param resultType 目标类型
     * @return 转换后的目标对象
     */
    private <T> T parseResponse(Object result, Class<T> resultType) {
        if (resultType == String.class) {
            return (T) result;
        }
        return JSON.parseObject(String.valueOf(result), resultType);
    }

    /**
     * 发送POST请求到LLM服务器并获取响应体字符串。
     *
     * @param req      请求对象
     * @param errorMsg 错误提示信息模板
     * @return 响应体字符串
     */
    private String doPostAndRequireBody(JsonRpcReq req, String errorMsg) {
        ResponseEntity<String> resp = restTemplate.postForEntity(LlmConfig.IP, req, String.class);

        if (!resp.getStatusCode().is2xxSuccessful()) {
            throw new RequestLlmFailException(errorMsg + "，HTTP状态码=" + resp.getStatusCodeValue());
        }
        String body = resp.getBody();
        if (body == null || body.trim().isEmpty()) {
            throw new RequestLlmFailException(errorMsg + "，返回体为空");
        }
        return body;
    }

    /**
     * 解析响应体字符串为JSONObject，并检查是否存在错误字段。
     *
     * @param body     响应体字符串
     * @param errorMsg 错误提示信息模板
     * @return 解析成功的JSONObject对象
     */
    private JSONObject parseJsonOrThrow(String body, String errorMsg) {
        try {
            JSONObject json = JSON.parseObject(body);

            if (json.containsKey("error") && json.get("error") != null) {
                JSONObject err = json.getJSONObject("error");
                String detail = String.format("JSON-RPC错误(code=%s, message=%s, data=%s)",
                        err.get("code"), err.get("message"), err.get("data"));
                throw new RequestLlmFailException(errorMsg + "，" + detail);
            }
            return json;
        } catch (Exception ex) {
            throw new RequestLlmFailException(errorMsg + "，解析JSON失败：" + ex.getMessage(), ex);
        }
    }

    /**
     * 提取JSON对象中的result字段内容。
     *
     * @param json     JSON响应对象
     * @param errorMsg 错误提示信息模板
     * @return result字段的内容
     */
    private Object requireResult(JSONObject json, String errorMsg) {
        Object result = json.get("result");
        if (result == null) {
            throw new RequestLlmFailException(errorMsg + "，result字段为空");
        }
        return result;
    }

    /**
     * 向LLM上传文件并接收响应结果。
     *
     * @param userId         用户ID
     * @param action         动作标识
     * @param url            文件上传地址
     * @param file           待上传的文件对象
     * @param resultCodeEnum 错误枚举值
     * @param clazz          响应结果的类型
     * @return 解析后的响应对象
     */
    private <T> T doPostFile(Integer userId, String action, String url,
                             MultipartFile file, ResultCodeEnum resultCodeEnum, Class<T> clazz) {

        File temp = null;
        Integer sessionId = TeacherUtils.generateSessionId(file);

        try {
            llmRequestLogsService.saveRequestLog(userId, action, sessionId, url, file);

            temp = File.createTempFile("llm_request_upload_",
                    Objects.requireNonNull(file.getOriginalFilename()));
            file.transferTo(temp);

            MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
            body.add("file", new FileSystemResource(temp));

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.MULTIPART_FORM_DATA);
            HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);

            ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
            String respBody = response.getBody();

            llmRequestLogsService.updateResponseLog(sessionId, respBody);

            if (respBody == null || respBody.trim().isEmpty()) {
                llmRequestLogsService.updateEmptyResponseLog(sessionId);
                throw new RequestLlmFailException(resultCodeEnum.getMsg() + "，返回体为空");
            }

            return JSON.parseObject(respBody, clazz);

        } catch (Exception e) {
            handleError(sessionId, e);
            throw new RequestLlmFailException(resultCodeEnum.getMsg() + "，接口调用失败：" + e.getMessage(), e);
        } finally {
            if (temp != null && temp.exists()) {
                temp.delete();
            }
        }
    }

    /**
     * 发送 JSON POST 请求
     *
     * @param userId         用户ID
     * @param action         动作标识
     * @param url            目标接口地址
     * @param payload        请求体对象（会序列化为 JSON）
     * @param resultCodeEnum 错误码枚举
     * @param clazz          返回类型
     */
    private <T> T doPostRaw(Integer userId, String action, String url,
                            Object payload, ResultCodeEnum resultCodeEnum, Class<T> clazz) {

        Integer sessionId = TeacherUtils.generateSessionId(payload);
        try {
            // 记录请求日志
            llmRequestLogsService.saveRequestLog(userId, action, sessionId, url, payload);
            // 构造 JSON 请求
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            String jsonBody = JSON.toJSONString(payload);
            HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers);
            // 执行 POST
            ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
            String respBody = response.getBody();
            // 写响应日志
            llmRequestLogsService.updateResponseLog(sessionId, respBody);
            if (respBody == null || respBody.trim().isEmpty()) {
                llmRequestLogsService.updateEmptyResponseLog(sessionId);
                throw new RequestLlmFailException(resultCodeEnum.getMsg() + "，返回体为空");
            }
            // 解析 JSON
            return JSON.parseObject(respBody, clazz);
        } catch (Exception e) {
            // 更新日志
            llmRequestLogsService.updateErrorLog(sessionId, e.getMessage());
            llmRequestLogsService.updateEmptyResponseLog(sessionId);
            throw new RequestLlmFailException(
                    resultCodeEnum.getMsg() + "，接口调用失败：" + e.getMessage(), e);
        }
    }

    /**
     * 请求LLM进行题目批改。
     *
     * @param params 题目批改请求参数
     * @return 批改结果响应对象
     */
    @Override
    public QuestionsCorrectLlmResult requestLlmForQuestionsCorrect(QuestionsCorrectLlmReq params) {
        params.setSessionId(TeacherUtils.generateSessionId(params));
        return executeRequest(params.getUserId(), "questionCorrect",
                JsonRpcReq.questionsCorrect(params),
                QuestionsCorrectLlmResult.class);
    }

    /**
     * 请求LLM推荐学习计划相关题目的ID集合。
     *
     * @param params 学习计划题目查询请求参数
     * @return 推荐题目的ID列表
     */
    @Override
    public List<Integer> requestLlmGetPlanQuestions(LlmGetPlanQuestionReq params) {
        params.setSessionId(TeacherUtils.generateSessionId(params));
        return executeRequest(params.getUserId(), "getPlanQuestions",
                JsonRpcReq.getPlanQuestions(params),
                List.class,
                Integer.class);
    }

    /**
     * 请求LLM推荐章节内容。
     *
     * @param userId 用户ID
     * @return 推荐章节响应列表
     */
    @Override
    public List<LlmRecommendChapterResult> requestLlmForRecommendChapters(Integer userId) {
        HashMap<String, Object> params = new HashMap<>();
        params.put("userId", userId);
        params.put("sessionId", TeacherUtils.generateSessionId(params));

        return executeRequest(userId, "getRecommendChapters",
                JsonRpcReq.recommendChapters(params),
                List.class,
                LlmRecommendChapterResult.class);
    }

    /**
     * 请求LLM执行语音转文字(STT)功能。
     *
     * @param file   语音文件
     * @param userId 用户ID
     * @return 转换后的文本内容
     */
    @Override
    public String requestLlmForStt(MultipartFile file, Integer userId) {
        try {
            byte[] audio = Base64.getEncoder().encode(file.getBytes());
            HashMap<String, Object> params = new HashMap<>();
            params.put("audio", audio);
            params.put("userId", userId);
            params.put("sessionId", TeacherUtils.generateSessionId(file, userId));
            return executeRequest(userId, "stt", JsonRpcReq.stt(params), String.class);
        } catch (IOException e) {
            throw new CommonException(ResultCodeEnum.REQUEST_LLM_FAIL);
        }
    }

    /**
     * 获取LLM生成的学生评语。
     *
     * @param userId 用户ID
     * @param score  科目得分情况列表
     * @return 评语响应对象
     */
    @Override
    public LlmCommentResult getLlmComment(Integer userId, List<SubjectScoreResp> score) {
        return executeRequest(userId, "getComment",
                JsonRpcReq.getComment(userId, score),
                LlmCommentResult.class);
    }

    /**
     * 构建发送至LLM的题目批改请求对象。
     *
     * @param userId 用户ID
     * @param type   题目类型
     * @param params 客户端传入的题目批改请求参数列表
     * @return 构造完成的LLM请求对象
     */
    @Override
    public QuestionsCorrectLlmReq buildLlmQuestionCorrectLlmReq(Integer userId,
                                                                Integer type,
                                                                List<QuestionCorrectReq> params,
                                                                Map<Integer, Integer> idQuestionIdMaps) {
        Map<Integer, String> idToAnswer = params.stream()
                .collect(Collectors.toMap(
                        QuestionCorrectReq::getId,
                        r -> String.join(",", r.getAnswers())
                ));
        Map<Integer, BankQuestionsEntity> questionMap = bankQuestionsService.listByIds(idQuestionIdMaps.keySet())
                .stream()
                .collect(Collectors.toMap(BankQuestionsEntity::getId, Function.identity()));
        List<QuestionCorrectLlmReq> reqQuestions = questionMap.values().stream().map(question -> {
            Integer answerId = idQuestionIdMaps.get(question.getId());
            String userAnswer = idToAnswer.getOrDefault(answerId, "");
            return QuestionConvert.INSTANCE.toQuestionCorrectLlmReq(
                    answerId,
                    question.getType(),
                    userAnswer,
                    TeacherUtils.removeHtmlTags(question.getHtml()),
                    question.getAnswer()
            );
        }).collect(Collectors.toList());
        return LlmConvert.INSTANCE.toQuestionsCorrectLlmResp(userId, type, reqQuestions);
    }

    /**
     * 对图片文件执行OCR识别。
     *
     * @param userId 用户ID
     * @param file   图片文件
     * @return OCR识别结果对象
     */
    @Override
    public BboxResult requestBbox(Integer userId, MultipartFile file) {
        return doPostFile(userId, "correctOcr", LlmConfig.OCR,
                file, ResultCodeEnum.OCR_FAIL, BboxResult.class);
    }

    @Override
    public ProcessUploadResult requestProcessUpload(Integer userId, String base64) {
        HashMap<String, Object> reqParams = new HashMap<>();
        reqParams.put("imgBase64", base64);
        return doPostRaw(userId, "processUpload", LlmConfig.PROCESS_UPLOAD,
                reqParams, ResultCodeEnum.PROCESS_UPLOAD_FAIL, ProcessUploadResult.class);
    }

    @Override
    public String requestContextAboutSingleQuestion(Integer userId, String url, List<Integer> bbox) {
        HashMap<String, Object> reqParams = new HashMap<>();
        reqParams.put("imgUrl", url);
        reqParams.put("bboxes", bbox);
        return doPostRaw(userId, "contextAboutSingleQuestion", LlmConfig.GET_CONTEXT_ABOUT_SINGLE_QUESTION,
                reqParams, ResultCodeEnum.CONTEXT_ABOUT_SINGLE_QUESTION_FAIL, String.class);
    }

    @Override
    public String requestOcrWithBase64(Integer userId, String cropedImgBase64) {
        HashMap<String, Object> reqParams = new HashMap<>();
        reqParams.put("cropedImgBase64", cropedImgBase64);
        return doPostRaw(userId, "getTextBase64", LlmConfig.GET_TEXT_BASE64,
                reqParams, ResultCodeEnum.OCR_TEXT_FAIL, String.class);
    }

}
