package tech.easyflow.skill.file;

import com.easyagents.skill.model.SkillScriptLanguage;
import com.easyagents.skill.util.SkillHashes;
import com.easyagents.skill.util.SkillPaths;
import com.mybatisflex.core.query.QueryWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.skill.entity.Skill;
import tech.easyflow.skill.entity.SkillAsset;
import tech.easyflow.skill.entity.SkillReference;
import tech.easyflow.skill.entity.SkillScript;
import tech.easyflow.skill.service.SkillAssetService;
import tech.easyflow.skill.service.SkillReferenceService;
import tech.easyflow.skill.service.SkillScriptService;
import tech.easyflow.skill.service.SkillService;
import tech.easyflow.skill.store.DBSkillContentStore;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.ResourceAccessService;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Skill 逻辑文件服务实现。
 */
@Service
public class SkillFileServiceImpl implements SkillFileService {

    private static final String DEFAULT_MEDIA_TYPE = "application/octet-stream";

    private final SkillService skillService;
    private final SkillReferenceService skillReferenceService;
    private final SkillScriptService skillScriptService;
    private final SkillAssetService skillAssetService;
    private final DBSkillContentStore contentStore;
    private final ResourceAccessService resourceAccessService;

    /**
     * 创建 Skill 逻辑文件服务。
     *
     * @param skillService Skill 服务
     * @param skillReferenceService reference 服务
     * @param skillScriptService script 服务
     * @param skillAssetService asset 服务
     * @param contentStore asset 内容存储
     * @param resourceAccessService 资源访问服务
     */
    public SkillFileServiceImpl(SkillService skillService,
                                SkillReferenceService skillReferenceService,
                                SkillScriptService skillScriptService,
                                SkillAssetService skillAssetService,
                                DBSkillContentStore contentStore,
                                ResourceAccessService resourceAccessService) {
        this.skillService = skillService;
        this.skillReferenceService = skillReferenceService;
        this.skillScriptService = skillScriptService;
        this.skillAssetService = skillAssetService;
        this.contentStore = contentStore;
        this.resourceAccessService = resourceAccessService;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<SkillFileNode> tree(BigInteger skillId) {
        Skill skill = requireReadableSkill(skillId);
        List<SkillFileNode> roots = new ArrayList<>();
        roots.add(fileNode(SkillPaths.SKILL_FILE, "SKILL.md", SkillFileType.SKILL.name()));
        roots.add(directoryNode(SkillPaths.REFERENCES_DIR, skill.getReferences().stream()
                .map(item -> fileNode(item.getPath(), SkillPaths.fileName(item.getPath()), SkillFileType.REFERENCE.name()))
                .toList()));
        roots.add(directoryNode(SkillPaths.SCRIPTS_DIR, skill.getScripts().stream()
                .map(item -> {
                    SkillFileNode node = fileNode(item.getPath(), SkillPaths.fileName(item.getPath()), SkillFileType.SCRIPT.name());
                    node.setLanguage(item.getLanguage());
                    return node;
                })
                .toList()));
        roots.add(directoryNode(SkillPaths.ASSETS_DIR, skill.getAssets().stream()
                .map(item -> {
                    SkillFileNode node = fileNode(item.getPath(), SkillPaths.fileName(item.getPath()), SkillFileType.ASSET.name());
                    node.setMediaType(item.getMediaType());
                    node.setSize(item.getSize());
                    return node;
                })
                .toList()));
        return roots;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SkillFileContent getContent(BigInteger skillId, String path) {
        Skill skill = requireReadableSkill(skillId);
        String normalizedPath = SkillPaths.normalize(path);
        if (SkillPaths.SKILL_FILE.equals(normalizedPath)) {
            SkillFileContent content = new SkillFileContent();
            content.setPath(SkillPaths.SKILL_FILE);
            content.setType(SkillFileType.SKILL.name());
            content.setContent(skill.getSkillContent());
            content.setSize((long) bytes(skill.getSkillContent()).length);
            return content;
        }
        String topDir = SkillPaths.firstSegment(normalizedPath);
        if (SkillPaths.REFERENCES_DIR.equals(topDir)) {
            SkillReference reference = requireReference(skillId, normalizedPath);
            SkillFileContent content = new SkillFileContent();
            content.setPath(reference.getPath());
            content.setType(SkillFileType.REFERENCE.name());
            content.setContent(reference.getContent());
            content.setSize(reference.getSize());
            return content;
        }
        if (SkillPaths.SCRIPTS_DIR.equals(topDir)) {
            SkillScript script = requireScript(skillId, normalizedPath);
            SkillFileContent content = new SkillFileContent();
            content.setPath(script.getPath());
            content.setType(SkillFileType.SCRIPT.name());
            content.setContent(script.getContent());
            content.setLanguage(script.getLanguage());
            content.setSize(script.getSize());
            return content;
        }
        if (SkillPaths.ASSETS_DIR.equals(topDir)) {
            SkillAsset asset = requireAsset(skillId, normalizedPath);
            SkillFileContent content = new SkillFileContent();
            content.setPath(asset.getPath());
            content.setType(SkillFileType.ASSET.name());
            content.setMediaType(asset.getMediaType());
            content.setSize(asset.getSize());
            content.setDownloadUrl("/api/v1/skill/file/asset");
            return content;
        }
        throw new BusinessException("不支持的 Skill 文件路径");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public SkillFileContent saveContent(SkillFileSaveRequest request) {
        if (request == null || request.getSkillId() == null) {
            throw new BusinessException("Skill ID 不能为空");
        }
        Skill skill = requireManageSkill(request.getSkillId());
        String normalizedPath = SkillPaths.normalize(request.getPath());
        String content = request.getContent() == null ? "" : request.getContent();
        if (SkillPaths.SKILL_FILE.equals(normalizedPath)) {
            skill.setSkillContent(content);
            skillService.updateDraft(skill);
            return getContent(skill.getId(), SkillPaths.SKILL_FILE);
        }
        String topDir = SkillPaths.firstSegment(normalizedPath);
        if (SkillPaths.REFERENCES_DIR.equals(topDir)) {
            saveReference(skill, normalizedPath, content);
            refreshSkillCounts(skill.getId());
            return getContent(skill.getId(), normalizedPath);
        }
        if (SkillPaths.SCRIPTS_DIR.equals(topDir)) {
            saveScript(skill, normalizedPath, content);
            refreshSkillCounts(skill.getId());
            return getContent(skill.getId(), normalizedPath);
        }
        throw new BusinessException("仅支持保存 SKILL.md、references/*.md 和 scripts/*.py|*.js|*.sh");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteFile(BigInteger skillId, String path) {
        requireManageSkill(skillId);
        String normalizedPath = SkillPaths.normalize(path);
        if (SkillPaths.SKILL_FILE.equals(normalizedPath)) {
            throw new BusinessException("SKILL.md 不允许删除");
        }
        String topDir = SkillPaths.firstSegment(normalizedPath);
        if (SkillPaths.REFERENCES_DIR.equals(topDir)) {
            skillReferenceService.remove(QueryWrapper.create().eq(SkillReference::getSkillId, skillId).eq(SkillReference::getPath, normalizedPath));
        } else if (SkillPaths.SCRIPTS_DIR.equals(topDir)) {
            skillScriptService.remove(QueryWrapper.create().eq(SkillScript::getSkillId, skillId).eq(SkillScript::getPath, normalizedPath));
        } else if (SkillPaths.ASSETS_DIR.equals(topDir)) {
            skillAssetService.remove(QueryWrapper.create().eq(SkillAsset::getSkillId, skillId).eq(SkillAsset::getPath, normalizedPath));
        } else {
            throw new BusinessException("不支持的 Skill 文件路径");
        }
        refreshSkillCounts(skillId);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public SkillFileContent uploadAsset(BigInteger skillId, String path, MultipartFile file) {
        Skill skill = requireManageSkill(skillId);
        if (file == null || file.isEmpty()) {
            throw new BusinessException("asset 文件不能为空");
        }
        String normalizedPath = normalizeAssetPath(path, file.getOriginalFilename());
        try {
            String contentRef = contentStore.put(file.getBytes());
            String hash = contentRef.substring("sha256:".length());
            SkillAsset asset = findAsset(skillId, normalizedPath);
            if (asset == null) {
                asset = new SkillAsset();
                asset.setTenantId(skill.getTenantId());
                asset.setSkillId(skillId);
                asset.setPath(normalizedPath);
            }
            asset.setName(SkillPaths.fileName(normalizedPath));
            asset.setMediaType(detectMediaType(normalizedPath));
            asset.setContentRef(contentRef);
            asset.setContentHash(hash);
            asset.setSize(file.getSize());
            if (asset.getId() == null) {
                skillAssetService.save(asset);
            } else {
                skillAssetService.updateById(asset);
            }
            refreshSkillCounts(skillId);
            return getContent(skillId, normalizedPath);
        } catch (IOException e) {
            throw new BusinessException("读取 asset 文件失败");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public InputStream openAsset(BigInteger skillId, String path) {
        requireReadableSkill(skillId);
        SkillAsset asset = requireAsset(skillId, SkillPaths.normalize(path));
        return contentStore.open(asset.getContentRef());
    }

    private Skill requireReadableSkill(BigInteger skillId) {
        Skill skill = requireSkill(skillId);
        resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.READ, "无权限查看该 Skill");
        return skillService.getDetail(skillId);
    }

    private Skill requireManageSkill(BigInteger skillId) {
        Skill skill = skillService.getDetail(skillId);
        resourceAccessService.assertAccess(CategoryResourceType.SKILL, skill, ResourceAction.MANAGE, "无权限管理该 Skill");
        return skill;
    }

    private Skill requireSkill(BigInteger skillId) {
        if (skillId == null) {
            throw new BusinessException("Skill ID 不能为空");
        }
        Skill skill = skillService.getById(skillId);
        if (skill == null) {
            throw new BusinessException("Skill 不存在");
        }
        return skill;
    }

    private void saveReference(Skill skill, String path, String content) {
        if (!SkillPaths.hasExtension(path, ".md")) {
            throw new BusinessException("reference 仅支持 .md 文件");
        }
        SkillReference reference = findReference(skill.getId(), path);
        if (reference == null) {
            reference = new SkillReference();
            reference.setTenantId(skill.getTenantId());
            reference.setSkillId(skill.getId());
            reference.setPath(path);
        }
        byte[] bytes = bytes(content);
        reference.setName(SkillPaths.fileName(path));
        reference.setContent(content);
        reference.setContentHash(SkillHashes.sha256Hex(bytes));
        reference.setSize((long) bytes.length);
        if (reference.getId() == null) {
            skillReferenceService.save(reference);
        } else {
            skillReferenceService.updateById(reference);
        }
    }

    private void saveScript(Skill skill, String path, String content) {
        SkillScriptLanguage language = SkillScriptLanguage.fromPath(path);
        if (language == SkillScriptLanguage.UNKNOWN) {
            throw new BusinessException("script 仅支持 .py、.js、.sh 文件");
        }
        SkillScript script = findScript(skill.getId(), path);
        if (script == null) {
            script = new SkillScript();
            script.setTenantId(skill.getTenantId());
            script.setSkillId(skill.getId());
            script.setPath(path);
        }
        byte[] bytes = bytes(content);
        script.setLanguage(language.name());
        script.setContent(content);
        script.setContentHash(SkillHashes.sha256Hex(bytes));
        script.setSize((long) bytes.length);
        if (script.getId() == null) {
            skillScriptService.save(script);
        } else {
            skillScriptService.updateById(script);
        }
    }

    private void refreshSkillCounts(BigInteger skillId) {
        Skill skill = skillService.getDetail(skillId);
        Skill update = new Skill();
        update.setId(skillId);
        update.setReferenceCount(skill.getReferences() == null ? 0 : skill.getReferences().size());
        update.setScriptCount(skill.getScripts() == null ? 0 : skill.getScripts().size());
        update.setAssetCount(skill.getAssets() == null ? 0 : skill.getAssets().size());
        skillService.updateById(update);
    }

    private SkillReference findReference(BigInteger skillId, String path) {
        List<SkillReference> records = skillReferenceService.list(QueryWrapper.create()
                .eq(SkillReference::getSkillId, skillId)
                .eq(SkillReference::getPath, path));
        return records.isEmpty() ? null : records.get(0);
    }

    private SkillReference requireReference(BigInteger skillId, String path) {
        SkillReference reference = findReference(skillId, path);
        if (reference == null) {
            throw new BusinessException("reference 文件不存在");
        }
        return reference;
    }

    private SkillScript findScript(BigInteger skillId, String path) {
        List<SkillScript> records = skillScriptService.list(QueryWrapper.create()
                .eq(SkillScript::getSkillId, skillId)
                .eq(SkillScript::getPath, path));
        return records.isEmpty() ? null : records.get(0);
    }

    private SkillScript requireScript(BigInteger skillId, String path) {
        SkillScript script = findScript(skillId, path);
        if (script == null) {
            throw new BusinessException("script 文件不存在");
        }
        return script;
    }

    private SkillAsset findAsset(BigInteger skillId, String path) {
        List<SkillAsset> records = skillAssetService.list(QueryWrapper.create()
                .eq(SkillAsset::getSkillId, skillId)
                .eq(SkillAsset::getPath, path));
        return records.isEmpty() ? null : records.get(0);
    }

    private SkillAsset requireAsset(BigInteger skillId, String path) {
        SkillAsset asset = findAsset(skillId, path);
        if (asset == null) {
            throw new BusinessException("asset 文件不存在");
        }
        return asset;
    }

    private String normalizeAssetPath(String path, String originalFilename) {
        String effectivePath = path;
        if (effectivePath == null || effectivePath.isBlank()) {
            effectivePath = SkillPaths.ASSETS_DIR + "/" + (originalFilename == null ? "asset.bin" : originalFilename);
        }
        String normalized = SkillPaths.normalize(effectivePath);
        if (!SkillPaths.ASSETS_DIR.equals(SkillPaths.firstSegment(normalized))) {
            throw new BusinessException("asset 必须位于 assets/ 目录下");
        }
        return normalized;
    }

    private SkillFileNode directoryNode(String name, List<SkillFileNode> flatChildren) {
        SkillFileNode node = new SkillFileNode();
        node.setKey(name);
        node.setPath(name);
        node.setName(name + "/");
        node.setType("DIRECTORY");
        node.setChildren(toNestedChildren(name, flatChildren));
        return node;
    }

    private List<SkillFileNode> toNestedChildren(String root, List<SkillFileNode> flatChildren) {
        Map<String, SkillFileNode> nodes = new LinkedHashMap<>();
        for (SkillFileNode child : flatChildren) {
            String[] segments = child.getPath().substring(root.length() + 1).split("/");
            String currentPath = root;
            for (int i = 0; i < segments.length; i++) {
                currentPath = currentPath + "/" + segments[i];
                boolean leaf = i == segments.length - 1;
                if (leaf) {
                    nodes.put(currentPath, child);
                } else {
                    String segmentName = segments[i];
                    nodes.computeIfAbsent(currentPath, key -> {
                        SkillFileNode directory = new SkillFileNode();
                        directory.setKey(key);
                        directory.setPath(key);
                        directory.setName(segmentName + "/");
                        directory.setType("DIRECTORY");
                        return directory;
                    });
                }
            }
        }
        List<SkillFileNode> roots = new ArrayList<>();
        for (SkillFileNode node : nodes.values()) {
            String parentPath = parentPath(node.getPath());
            if (root.equals(parentPath)) {
                roots.add(node);
            } else {
                SkillFileNode parent = nodes.get(parentPath);
                if (parent != null) {
                    parent.getChildren().add(node);
                }
            }
        }
        return roots;
    }

    private SkillFileNode fileNode(String path, String name, String type) {
        SkillFileNode node = new SkillFileNode();
        node.setKey(path);
        node.setPath(path);
        node.setName(name);
        node.setType(type);
        return node;
    }

    private String parentPath(String path) {
        int index = path.lastIndexOf('/');
        return index < 0 ? "" : path.substring(0, index);
    }

    private byte[] bytes(String content) {
        return (content == null ? "" : content).getBytes(StandardCharsets.UTF_8);
    }

    private String detectMediaType(String path) {
        String mediaType = URLConnection.guessContentTypeFromName(path);
        return mediaType == null ? DEFAULT_MEDIA_TYPE : mediaType;
    }
}
