package com.hertz.modules.knowledge.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hertz.common.exception.BusinessException;
import com.hertz.config.AppPathResolver;
import com.hertz.config.AppProperties;
import com.hertz.modules.knowledge.entity.KnowledgeBase;
import com.hertz.modules.knowledge.entity.KnowledgeDocument;
import com.hertz.modules.knowledge.mapper.KnowledgeBaseMapper;
import com.hertz.modules.knowledge.mapper.KnowledgeDocumentMapper;
import com.hertz.modules.knowledge.service.KnowledgeBaseService;
import com.hertz.modules.knowledge.service.KnowledgeVectorStoreManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.core.io.FileSystemResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.security.MessageDigest;

@Service
@RequiredArgsConstructor
@Slf4j
public class KnowledgeBaseServiceImpl implements KnowledgeBaseService {

    private final KnowledgeBaseMapper knowledgeBaseMapper;
    private final KnowledgeDocumentMapper knowledgeDocumentMapper;
    private final KnowledgeVectorStoreManager vectorStoreManager;
    private final EmbeddingModel embeddingModel;
    private final AppProperties appProperties;
    private final AppPathResolver pathResolver;

    private static final Pattern OLLAMA_MODEL_NOT_FOUND = Pattern.compile("model\\\\s+\\\\\\\"([^\\\\\\\"]+)\\\\\\\"\\\\s+not\\\\s+found", Pattern.CASE_INSENSITIVE);
    private static final String STATUS_PROCESSING = "PROCESSING";
    private static final String STATUS_READY = "READY";
    private static final String STATUS_FAILED = "FAILED";

    @Override
    public KnowledgeBase createKnowledgeBase(String name, String description) {
        KnowledgeBase kb = new KnowledgeBase();
        kb.setName(name);
        kb.setDescription(description);
        kb.setCreateTime(LocalDateTime.now());
        kb.setCreateBy(com.hertz.security.SecurityUtils.getCurrentUserId());
        knowledgeBaseMapper.insert(kb);
        return kb;
    }

    @Override
    public IPage<KnowledgeBase> pageKnowledgeBases(int page, int size, String keyword) {
        LambdaQueryWrapper<KnowledgeBase> wrapper = new LambdaQueryWrapper<KnowledgeBase>()
            .orderByDesc(KnowledgeBase::getCreateTime);
        if (keyword != null && !keyword.isBlank()) {
            wrapper.and(w -> w.like(KnowledgeBase::getName, keyword)
                .or().like(KnowledgeBase::getDescription, keyword));
        }
        return knowledgeBaseMapper.selectPage(Page.of(page, size), wrapper);
    }

    @Override
    public void uploadDocument(Long kbId, MultipartFile file) {
        String originalName = file.getOriginalFilename();
        log.info("Uploading document to KB {}: {}", kbId, originalName);

        if (kbId == null || knowledgeBaseMapper.selectById(kbId) == null) {
            throw new BusinessException(40400, "知识库不存在");
        }
        if (file.isEmpty()) {
            throw new BusinessException(40001, "文件不能为空");
        }
        if (originalName == null || originalName.isBlank()) {
            throw new BusinessException(40001, "文件名不能为空");
        }

        String suffix = "";
        int dot = originalName.lastIndexOf(".");
        if (dot >= 0 && dot < originalName.length() - 1) {
            suffix = originalName.substring(dot);
        }
        String storedName = UUID.randomUUID().toString().replace("-", "") + suffix;
        String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
        String relativePath = normalizePath(appProperties.getUpload().getKnowledgePath())
            + "kb-" + kbId + "/" + datePath + "/" + storedName;
        String fullPath = pathResolver.resolve(appProperties.getUpload().getRootPath()) + File.separator + relativePath;
        File dest = new File(fullPath);
        if (!dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }

        KnowledgeDocument docRecord = new KnowledgeDocument();
        docRecord.setKbId(kbId);
        docRecord.setOriginalName(originalName);
        docRecord.setStoredName(storedName);
        docRecord.setStoredPath(relativePath);
        docRecord.setContentType(file.getContentType());
        docRecord.setSizeBytes(file.getSize());
        docRecord.setStatus(STATUS_PROCESSING);
        docRecord.setChunkCount(0);
        docRecord.setCreateTime(LocalDateTime.now());
        knowledgeDocumentMapper.insert(docRecord);

        try {
            file.transferTo(dest);
            docRecord.setSha256(sha256Hex(dest));
            knowledgeDocumentMapper.updateById(docRecord);

            TikaDocumentReader reader = new TikaDocumentReader(new FileSystemResource(dest));
            List<Document> documents = reader.read();

            for (Document doc : documents) {
                doc.getMetadata().put("kbId", kbId);
                doc.getMetadata().put("documentId", docRecord.getId());
                doc.getMetadata().put("filename", originalName);
            }

            TokenTextSplitter splitter = new TokenTextSplitter();
            List<Document> splitDocuments = splitter.apply(documents);

            SimpleVectorStore store = vectorStoreManager.get(kbId);
            try {
                store.add(splitDocuments);
            } catch (RuntimeException e) {
                throw translateVectorizeException(e);
            }
            vectorStoreManager.save(kbId);
            
            log.info("Added {} chunks to vector store for KB {}", splitDocuments.size(), kbId);
            docRecord.setStatus(STATUS_READY);
            docRecord.setChunkCount(splitDocuments.size());
            docRecord.setErrorMessage(null);
            knowledgeDocumentMapper.updateById(docRecord);

        } catch (Exception e) {
            docRecord.setStatus(STATUS_FAILED);
            docRecord.setErrorMessage(truncate(e.getMessage(), 1000));
            knowledgeDocumentMapper.updateById(docRecord);
            throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
        }
    }

    @Override
    public List<KnowledgeDocument> listDocuments(Long kbId) {
        return knowledgeDocumentMapper.selectList(new LambdaQueryWrapper<KnowledgeDocument>()
            .eq(KnowledgeDocument::getKbId, kbId)
            .orderByDesc(KnowledgeDocument::getCreateTime));
    }

    @Override
    public void deleteDocument(Long kbId, Long documentId) {
        KnowledgeDocument doc = knowledgeDocumentMapper.selectById(documentId);
        if (doc == null || !kbId.equals(doc.getKbId())) {
            throw new BusinessException(40400, "文档不存在");
        }

        File file = new File(pathResolver.resolve(appProperties.getUpload().getRootPath()) + File.separator + doc.getStoredPath());
        if (file.exists()) {
            file.delete();
        }
        knowledgeDocumentMapper.deleteById(documentId);

        rebuildVectorStore(kbId);
    }

    @Override
    public void rebuildVectorStore(Long kbId) {
        List<KnowledgeDocument> docs = knowledgeDocumentMapper.selectList(new LambdaQueryWrapper<KnowledgeDocument>()
            .eq(KnowledgeDocument::getKbId, kbId)
            .eq(KnowledgeDocument::getStatus, STATUS_READY)
            .orderByAsc(KnowledgeDocument::getId));

        SimpleVectorStore newStore = SimpleVectorStore.builder(embeddingModel).build();
        TokenTextSplitter splitter = new TokenTextSplitter();

        int totalChunks = 0;
        for (KnowledgeDocument doc : docs) {
            File file = new File(pathResolver.resolve(appProperties.getUpload().getRootPath()) + File.separator + doc.getStoredPath());
            if (!file.exists()) {
                continue;
            }
            TikaDocumentReader reader = new TikaDocumentReader(new FileSystemResource(file));
            List<Document> documents = reader.read();
            for (Document d : documents) {
                d.getMetadata().put("kbId", kbId);
                d.getMetadata().put("documentId", doc.getId());
                d.getMetadata().put("filename", doc.getOriginalName());
            }
            List<Document> split = splitter.apply(documents);
            newStore.add(split);
            totalChunks += split.size();
            doc.setChunkCount(split.size());
            knowledgeDocumentMapper.updateById(doc);
        }

        vectorStoreManager.replace(kbId, newStore);
        log.info("Rebuilt vector store for KB {} with {} documents and {} chunks", kbId, docs.size(), totalChunks);
    }

    private RuntimeException translateVectorizeException(RuntimeException e) {
        var message = e.getMessage();
        if (message == null) {
            return e;
        }

        if (message.contains("[404]") && message.toLowerCase().contains("not found") && message.toLowerCase().contains("try pulling it first")) {
            String model = null;
            Matcher matcher = OLLAMA_MODEL_NOT_FOUND.matcher(message);
            if (matcher.find()) {
                model = matcher.group(1);
            }
            if (model == null || model.isBlank()) {
                model = "nomic-embed-text";
            }
            return new BusinessException(50300, "Ollama 嵌入模型未安装: " + model + "。请先执行：ollama pull " + model);
        }
        return e;
    }

    private String normalizePath(String path) {
        if (path == null || path.isBlank()) {
            return "";
        }
        return path.endsWith("/") || path.endsWith("\\") ? path.replace("\\", "/") : path.replace("\\", "/") + "/";
    }

    private String sha256Hex(File file) {
        try (InputStream in = java.nio.file.Files.newInputStream(file.toPath())) {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] buffer = new byte[8192];
            int read;
            while ((read = in.read(buffer)) > 0) {
                digest.update(buffer, 0, read);
            }
            byte[] bytes = digest.digest();
            StringBuilder sb = new StringBuilder(bytes.length * 2);
            for (byte b : bytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            return null;
        }
    }

    private String truncate(String s, int max) {
        if (s == null) return null;
        if (s.length() <= max) return s;
        return s.substring(0, max);
    }

    @Override
    public void deleteKnowledgeBase(Long id) {
        List<KnowledgeDocument> docs = knowledgeDocumentMapper.selectList(new LambdaQueryWrapper<KnowledgeDocument>()
            .eq(KnowledgeDocument::getKbId, id));
        for (KnowledgeDocument doc : docs) {
            File file = new File(pathResolver.resolve(appProperties.getUpload().getRootPath()) + File.separator + doc.getStoredPath());
            if (file.exists()) {
                file.delete();
            }
            knowledgeDocumentMapper.deleteById(doc.getId());
        }
        vectorStoreManager.delete(id);
        knowledgeBaseMapper.deleteById(id);
    }
}
