package com.ruoyi.web.kb.service.impl;

import java.io.File;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.function.Supplier;

import com.ruoyi.web.kb.support.KbApiProperties;
import com.ruoyi.web.kb.service.KnowledgeBaseClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
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.HttpStatusCodeException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;

/**
 * 调用大模型知识库开放 API（multipart 上传 / 删除文件等）。
 */
@Service
public class KnowledgeBaseClientImpl implements KnowledgeBaseClient
{
    private static final Logger log = LoggerFactory.getLogger(KnowledgeBaseClientImpl.class);

    private static final String[] FILE_ID_KEYS = { "file_id", "fileId", "kbDocId", "kb_doc_id", "docId", "id" };

    private final KbApiProperties properties;

    private final RestTemplate restTemplate;

    private final ObjectMapper objectMapper;

    public KnowledgeBaseClientImpl(KbApiProperties properties,
            @Qualifier("kbRestTemplate") RestTemplate restTemplate,
            ObjectMapper objectMapper)
    {
        this.properties = properties;
        this.restTemplate = restTemplate;
        this.objectMapper = objectMapper;
    }

    @Override
    public String uploadFile(File file, String uploadFilename, String knowledgeBase, String category)
    {
        if (!properties.isEnabled())
        {
            throw new ServiceException("知识库远程调用已关闭（ruoyi.kb.enabled=false）");
        }
        if (StringUtils.isEmpty(properties.getApiKey()))
        {
            throw new ServiceException("未配置知识库访问密钥 ruoyi.kb.api-key");
        }
        if (file == null || !file.isFile())
        {
            throw new ServiceException("上传文件无效或不存在");
        }
        String kb = StringUtils.isNotEmpty(knowledgeBase) ? knowledgeBase.trim()
                : properties.getDefaultKnowledgeBase();
        if (StringUtils.isEmpty(kb))
        {
            kb = "default";
        }
        String name = StringUtils.isNotEmpty(uploadFilename) ? uploadFilename.trim() : file.getName();

        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add("knowledge_base", kb);
        body.add("category", category);
        body.add("file", multipartFileResource(file, name));

        return postMultipart(body);
    }

    @Override
    public String uploadFileByUrl(String fileUrl, String category, String description)
    {
        if (!properties.isEnabled())
        {
            throw new ServiceException("知识库远程调用已关闭（ruoyi.kb.enabled=false）");
        }
        if (StringUtils.isEmpty(properties.getApiKey()))
        {
            throw new ServiceException("未配置知识库访问密钥 ruoyi.kb.api-key");
        }
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add("url", fileUrl);
        body.add("category", category);
        body.add("description", description);
        return postMultipart(body);
    }

    private String postMultipart(MultiValueMap<String, Object> body)
    {
        String url = properties.baseUrl() + "/api/v1/kb/files";
        HttpHeaders headers = new HttpHeaders();
        headers.add(properties.getAuthHeaderName(), properties.authHeaderValue());
        HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
        String raw = executeWithRetry("知识库上传", () -> {
            try
            {
                ResponseEntity<String> resp = restTemplate.exchange(URI.create(url), HttpMethod.POST, entity, String.class);
                return resp.getBody();
            }
            catch (HttpStatusCodeException e)
            {
                String b = e.getResponseBodyAsString(StandardCharsets.UTF_8);
                throw new ServiceException("知识库上传失败 HTTP " + e.getRawStatusCode() + "：" + abbreviate(b));
            }
        });
        return parseUploadResponse(raw);
    }

    private static FileSystemResource multipartFileResource(File file, String uploadName)
    {
        return new FileSystemResource(file)
        {
            @Override
            public String getFilename()
            {
                return uploadName;
            }
        };
    }

    @Override
    public void deleteFile(String kbDocId)
    {
        if (!properties.isEnabled())
        {
            throw new ServiceException("知识库远程调用已关闭（ruoyi.kb.enabled=false）");
        }
        if (StringUtils.isEmpty(properties.getApiKey()))
        {
            throw new ServiceException("未配置知识库访问密钥 ruoyi.kb.api-key");
        }
        if (StringUtils.isEmpty(kbDocId))
        {
            throw new ServiceException("知识库文档 id 为空");
        }
        String url = properties.baseUrl() + "/api/v1/kb/files/" + encodePathSegment(kbDocId);
        HttpHeaders headers = new HttpHeaders();
        headers.add(properties.getAuthHeaderName(), properties.authHeaderValue());
        HttpEntity<Void> entity = new HttpEntity<>(headers);
        executeWithRetry("知识库删除", () -> {
            try
            {
                ResponseEntity<String> resp = restTemplate.exchange(URI.create(url), HttpMethod.DELETE, entity, String.class);
                if (!resp.getStatusCode().is2xxSuccessful())
                {
                    throw new ServiceException("知识库删除失败 HTTP " + resp.getStatusCodeValue());
                }
                return null;
            }
            catch (HttpStatusCodeException e)
            {
                if (e.getRawStatusCode() == 404)
                {
                    return null;
                }
                String b = e.getResponseBodyAsString(StandardCharsets.UTF_8);
                throw new ServiceException("知识库删除失败 HTTP " + e.getRawStatusCode() + "：" + abbreviate(b));
            }
        });
    }

    /**
     * 超时或 transient 失败时重试，最多 {@link KbApiProperties#getMaxRetries()} 次（不含首次请求）。
     */
    private <T> T executeWithRetry(String operation, Supplier<T> action)
    {
        int maxRetries = Math.max(0, properties.getMaxRetries());
        Exception last = null;
        for (int attempt = 0; attempt <= maxRetries; attempt++)
        {
            try
            {
                return action.get();
            }
            catch (ServiceException e)
            {
                if (attempt >= maxRetries || !isRetryable(e))
                {
                    throw e;
                }
                last = e;
                log.warn("{}失败，第 {}/{} 次重试：{}", operation, attempt + 1, maxRetries, e.getMessage());
                sleepBeforeRetry();
            }
            catch (Exception e)
            {
                ServiceException wrapped = wrapUnexpected(operation, e);
                if (attempt >= maxRetries || !isRetryable(wrapped, e))
                {
                    throw wrapped;
                }
                last = wrapped;
                log.warn("{}异常，第 {}/{} 次重试：{}", operation, attempt + 1, maxRetries, e.getMessage());
                sleepBeforeRetry();
            }
        }
        if (last instanceof ServiceException)
        {
            throw (ServiceException) last;
        }
        throw wrapUnexpected(operation, last);
    }

    private static boolean isRetryable(ServiceException e)
    {
        String msg = e.getMessage();
        if (msg == null)
        {
            return false;
        }
        if (msg.contains("HTTP 404"))
        {
            return false;
        }
        if (msg.contains("HTTP 408") || msg.contains("HTTP 429"))
        {
            return true;
        }
        if (msg.matches(".*HTTP [45]\\d\\d.*"))
        {
            int code = extractHttpStatus(msg);
            return code >= 500 || code == 408 || code == 429;
        }
        return false;
    }

    private static boolean isRetryable(ServiceException wrapped, Exception cause)
    {
        if (cause instanceof ResourceAccessException)
        {
            return true;
        }
        return isRetryable(wrapped);
    }

    private static int extractHttpStatus(String msg)
    {
        int idx = msg.indexOf("HTTP ");
        if (idx < 0)
        {
            return 0;
        }
        int start = idx + 5;
        int end = start;
        while (end < msg.length() && Character.isDigit(msg.charAt(end)))
        {
            end++;
        }
        if (end == start)
        {
            return 0;
        }
        try
        {
            return Integer.parseInt(msg.substring(start, end));
        }
        catch (NumberFormatException e)
        {
            return 0;
        }
    }

    private void sleepBeforeRetry()
    {
        int interval = Math.max(0, properties.getRetryIntervalMs());
        if (interval <= 0)
        {
            return;
        }
        try
        {
            Thread.sleep(interval);
        }
        catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();
        }
    }

    private static ServiceException wrapUnexpected(String operation, Exception e)
    {
        if (e instanceof ServiceException)
        {
            return (ServiceException) e;
        }
        return new ServiceException(operation + "异常：" + e.getMessage());
    }

    private static String encodePathSegment(String id)
    {
        try
        {
            return java.net.URLEncoder.encode(id, StandardCharsets.UTF_8.name()).replace("+", "%20");
        }
        catch (java.io.UnsupportedEncodingException e)
        {
            return id;
        }
    }

    private String parseUploadResponse(String raw)
    {
        if (StringUtils.isEmpty(raw))
        {
            throw new ServiceException("知识库上传返回空响应");
        }
        JsonNode root;
        try
        {
            root = objectMapper.readTree(raw);
        }
        catch (Exception e)
        {
            throw new ServiceException("知识库上传响应非 JSON：" + abbreviate(raw));
        }
        assertBusinessOk(root);
        String fileId = extractFileId(root);
        if (StringUtils.isEmpty(fileId))
        {
            throw new ServiceException("知识库上传成功但未解析到 file_id，响应：" + abbreviate(raw));
        }
        return fileId;
    }

    private void assertBusinessOk(JsonNode root)
    {
        if (root == null || !root.has("code"))
        {
            return;
        }
        JsonNode c = root.get("code");
        boolean ok = false;
        if (c.isNumber())
        {
            int v = c.intValue();
            ok = (v == 0 || v == 200);
        }
        else if (c.isTextual())
        {
            String s = c.asText();
            ok = "0".equals(s) || "200".equals(s) || "success".equalsIgnoreCase(s);
        }
        if (!ok)
        {
            String msg = root.has("message") ? root.get("message").asText(null) : null;
            if (StringUtils.isEmpty(msg) && root.has("msg"))
            {
                msg = root.get("msg").asText(null);
            }
            if (StringUtils.isEmpty(msg))
            {
                msg = "知识库接口返回失败";
            }
            throw new ServiceException(msg);
        }
    }

    private static String extractFileId(JsonNode root)
    {
        JsonNode data = root.get("data");
        if (data != null && data.isTextual())
        {
            String t = data.asText(null);
            if (StringUtils.isNotEmpty(t))
            {
                return t;
            }
        }
        if (data != null && data.isObject())
        {
            for (String key : FILE_ID_KEYS)
            {
                if (data.has(key) && !data.get(key).isNull())
                {
                    String v = data.get(key).asText(null);
                    if (StringUtils.isNotEmpty(v))
                    {
                        return v;
                    }
                }
            }
        }
        for (String key : FILE_ID_KEYS)
        {
            if (root.has(key) && !root.get(key).isNull())
            {
                String v = root.get(key).asText(null);
                if (StringUtils.isNotEmpty(v))
                {
                    return v;
                }
            }
        }
        return null;
    }

    private static String abbreviate(String s)
    {
        if (s == null)
        {
            return "";
        }
        String t = s.trim();
        if (t.length() > 500)
        {
            return t.substring(0, 500) + "...";
        }
        return t;
    }
}
