package tech.easyflow.skill.gitimport;

import com.easyagents.skill.exception.SkillValidationException;
import com.easyagents.skill.factory.SkillFactory;
import com.easyagents.skill.model.Skill;
import com.easyagents.skill.model.SkillPackageLimits;
import com.easyagents.skill.util.SkillHashes;
import com.easyagents.skill.util.SkillPaths;
import com.easyagents.skill.util.SkillResources;
import com.easyagents.skill.validation.SkillValidationIssue;
import com.easyagents.skill.validation.SkillValidationSeverity;
import com.easyagents.skill.validation.defaults.DefaultSkillValidator;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.io.IOException;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 固定 Git Tree 中的标准 Skill 候选发现、校验与标准包组装器。
 */
@Component
public class SkillGitCandidateScanner {

    private static final Logger LOG = LoggerFactory.getLogger(SkillGitCandidateScanner.class);
    private static final int LFS_POINTER_MAX_BYTES = 1_024;
    private static final byte[] LFS_HEADER = "version https://git-lfs.github.com/spec/v1"
            .getBytes(StandardCharsets.UTF_8);

    private final SkillGitImportProperties properties;
    private final SkillPackageLimits packageLimits = SkillPackageLimits.defaults();
    private final DefaultSkillValidator validator = new DefaultSkillValidator();

    /**
     * 创建候选扫描器。
     *
     * @param properties Git 导入配置
     */
    public SkillGitCandidateScanner(SkillGitImportProperties properties) {
        this.properties = properties;
    }

    /**
     * 扫描快照固定提交中的全部标准 Skill 候选。
     *
     * @param snapshot 固定提交快照
     * @return 候选及其资源归属
     * @throws BusinessException 仓库结构不可读取或超过安全限额
     */
    public SkillGitRepositoryScan scan(SkillGitRepositoryReader.Snapshot snapshot) {
        Repository repository = snapshot.repository();
        try (RevWalk revWalk = new RevWalk(repository)) {
            RevCommit commit = revWalk.parseCommit(ObjectId.fromString(snapshot.commitSha()));
            List<RawEntry> entries = readEntries(repository, commit);
            Set<String> roots = discoverCandidateRoots(entries);
            if (roots.size() > properties.getMaxCandidates()) {
                throw new BusinessException(413, 4133, "Git 仓库中的 Skill 候选数量超过限制");
            }
            Map<String, List<RawEntry>> ownedEntries = assignEntries(entries, roots);
            List<SkillGitScanResult.Candidate> candidates = new ArrayList<>(roots.size());
            Map<String, List<SkillGitRepositoryScan.RepositoryFile>> filesByCandidate = new LinkedHashMap<>();
            roots.stream().sorted().forEach(root -> {
                CandidateBuild build = buildCandidate(repository, snapshot.commitSha(), root,
                        ownedEntries.getOrDefault(root, List.of()));
                candidates.add(build.candidate());
                filesByCandidate.put(build.candidate().candidateId(), build.files());
            });
            return new SkillGitRepositoryScan(candidates, filesByCandidate);
        } catch (IOException exception) {
            throw new BusinessException(500, 500, "读取 Git 仓库文件树失败", exception);
        }
    }

    /**
     * 将一个可导入候选组装为包含单个标准 Skill 的 ZIP。
     *
     * @param snapshot 固定提交快照
     * @param scan 同一快照的扫描数据
     * @param candidateId 候选 ID
     * @return 待交给标准包预检的临时 ZIP
     * @throws BusinessException 候选不存在、不可导入或文件读取失败
     */
    public Path createStandardZip(SkillGitRepositoryReader.Snapshot snapshot,
                                  SkillGitRepositoryScan scan,
                                  String candidateId) {
        SkillGitScanResult.Candidate candidate = scan.candidates().stream()
                .filter(item -> item.candidateId().equals(candidateId))
                .findFirst()
                .orElseThrow(() -> new BusinessException("Git Skill 候选不存在，请重新扫描"));
        if (!candidate.importable()) {
            throw new BusinessException("Git Skill 候选存在校验问题，不能导入：" + candidate.path());
        }
        List<SkillGitRepositoryScan.RepositoryFile> files = scan.filesByCandidate().get(candidateId);
        if (files == null || files.stream().noneMatch(file -> SkillPaths.SKILL_FILE.equals(file.relativePath()))) {
            throw new BusinessException("Git Skill 候选缺少 SKILL.md，请重新扫描");
        }
        Path archive;
        try {
            archive = Files.createTempFile("easyflow-git-skill-", ".zip");
        } catch (IOException exception) {
            throw new BusinessException(500, 500, "创建 Git Skill 临时包失败", exception);
        }
        try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(archive))) {
            for (SkillGitRepositoryScan.RepositoryFile file : files.stream()
                    .sorted(Comparator.comparing(SkillGitRepositoryScan.RepositoryFile::relativePath)).toList()) {
                FileMode mode = FileMode.fromBits(file.fileModeBits());
                if (!isRegular(mode)) {
                    throw new BusinessException("Git Skill 包含不支持的文件对象：" + file.relativePath());
                }
                ZipEntry entry = new ZipEntry(candidate.name() + "/" + file.relativePath());
                entry.setTime(0L);
                output.putNextEntry(entry);
                snapshot.repository().open(file.objectId(), Constants.OBJ_BLOB).copyTo(output);
                output.closeEntry();
            }
            return archive;
        } catch (IOException | RuntimeException exception) {
            deleteArchive(archive);
            if (exception instanceof BusinessException businessException) {
                throw businessException;
            }
            throw new BusinessException(500, 500, "组装 Git Skill 标准包失败", exception);
        }
    }

    /**
     * 删除扫描器创建的临时标准包。
     *
     * @param archive 临时 ZIP
     */
    public void deleteArchive(Path archive) {
        if (archive == null) {
            return;
        }
        try {
            Files.deleteIfExists(archive);
        } catch (IOException exception) {
            // 临时包删除失败不能覆盖已生成的导入预览；记录完整路径供运维清理。
            LOG.warn("清理 Git Skill 临时包失败，path={}", archive, exception);
        }
    }

    private List<RawEntry> readEntries(Repository repository, RevCommit commit) throws IOException {
        List<RawEntry> entries = new ArrayList<>();
        try (TreeWalk treeWalk = new TreeWalk(repository)) {
            treeWalk.addTree(commit.getTree());
            treeWalk.setRecursive(true);
            while (treeWalk.next()) {
                if (entries.size() >= properties.getMaxRepositoryFiles()) {
                    throw new BusinessException(413, 4134, "Git 仓库文件数量超过限制");
                }
                FileMode mode = treeWalk.getFileMode(0);
                ObjectId objectId = treeWalk.getObjectId(0).copy();
                long size = isRegular(mode) || FileMode.SYMLINK.equals(mode)
                        ? repository.open(objectId, Constants.OBJ_BLOB).getSize() : 0L;
                entries.add(new RawEntry(treeWalk.getPathString(), objectId, mode, size));
            }
        }
        return entries;
    }

    private Set<String> discoverCandidateRoots(List<RawEntry> entries) {
        Set<String> roots = new LinkedHashSet<>();
        for (RawEntry entry : entries) {
            if (isRegular(entry.mode()) && SkillPaths.SKILL_FILE.equals(fileName(entry.path()))) {
                roots.add(parentPath(entry.path()));
            }
        }
        return roots;
    }

    private Map<String, List<RawEntry>> assignEntries(List<RawEntry> entries, Set<String> roots) {
        Map<String, List<RawEntry>> result = new HashMap<>();
        roots.forEach(root -> result.put(root, new ArrayList<>()));
        for (RawEntry entry : entries) {
            String owner = nearestCandidateRoot(entry.path(), roots);
            if (owner != null) {
                result.get(owner).add(entry);
            }
        }
        return result;
    }

    private String nearestCandidateRoot(String filePath, Set<String> roots) {
        String parent = parentPath(filePath);
        while (parent != null) {
            if (roots.contains(parent)) {
                return parent;
            }
            parent = parent.isEmpty() ? null : parentPath(parent);
        }
        return null;
    }

    private CandidateBuild buildCandidate(Repository repository, String commitSha, String root,
                                          List<RawEntry> ownedEntries) {
        RawEntry skillFile = ownedEntries.stream()
                .filter(entry -> isRegular(entry.mode()))
                .filter(entry -> SkillPaths.SKILL_FILE.equals(relativePath(root, entry.path())))
                .findFirst().orElseThrow(() -> new BusinessException("Git Skill 候选缺少 SKILL.md"));
        List<SkillGitScanResult.Issue> issues = new ArrayList<>();
        String fallbackName = root.isEmpty() ? "root-skill" : fileName(root);
        String name = fallbackName;
        String description = "";
        String skillContent = null;
        if (skillFile.size() > packageLimits.getMaxTextFileBytes()) {
            issues.add(issue("TEXT_FILE_SIZE_LIMIT", "SKILL.md 超过允许大小", SkillPaths.SKILL_FILE));
        } else {
            try {
                skillContent = decodeUtf8(loadBytes(repository, skillFile));
                Skill skill = SkillFactory.create(skillContent);
                skill.setPackageRoot(root.isEmpty() ? skill.getName() : fileName(root));
                name = skill.getName();
                description = skill.getDescription();
                validator.validateReport(skill, packageLimits).getIssues().stream()
                        .filter(item -> item.getSeverity() == SkillValidationSeverity.ERROR)
                        .map(this::toIssue)
                        .forEach(issues::add);
            } catch (SkillValidationException exception) {
                issues.add(issue(exception.getCode() == null ? "INVALID_SKILL" : exception.getCode(),
                        exception.getMessage(), SkillPaths.SKILL_FILE));
            } catch (CharacterCodingException exception) {
                issues.add(issue("INVALID_UTF8", "SKILL.md 不是有效的 UTF-8 文本", SkillPaths.SKILL_FILE));
            } catch (IOException exception) {
                throw new BusinessException(500, 500, "读取 Git SKILL.md 失败", exception);
            }
        }

        List<SkillGitRepositoryScan.RepositoryFile> files = new ArrayList<>(ownedEntries.size());
        Set<String> collisionKeys = new HashSet<>();
        collisionKeys.add(SkillPaths.collisionKey(SkillPaths.SKILL_FILE));
        long totalBytes = 0L;
        int resourceCount = 0;
        for (RawEntry entry : ownedEntries) {
            String relative = relativePath(root, entry.path());
            totalBytes = addSize(totalBytes, entry.size());
            files.add(new SkillGitRepositoryScan.RepositoryFile(entry.path(), relative,
                    entry.objectId(), entry.size(), entry.mode().getBits()));
            if (SkillPaths.SKILL_FILE.equals(relative)) {
                continue;
            }
            resourceCount++;
            validateResource(repository, entry, relative, collisionKeys, issues);
        }
        if (resourceCount + 1 > packageLimits.getMaxEntryCount()) {
            issues.add(issue("ENTRY_COUNT_LIMIT", "Skill 文件数量超过允许上限", null));
        }
        if (totalBytes > packageLimits.getMaxTotalUncompressedBytes()) {
            issues.add(issue("TOTAL_SIZE_LIMIT", "Skill 资源总大小超过允许上限", null));
        }
        String candidateId = SkillHashes.sha256Hex((commitSha + "\n" + root + "\n"
                + skillFile.objectId().name()).getBytes(StandardCharsets.UTF_8));
        SkillGitScanResult.CandidateStatus status = issues.isEmpty()
                ? SkillGitScanResult.CandidateStatus.IMPORTABLE
                : SkillGitScanResult.CandidateStatus.NEEDS_ATTENTION;
        SkillGitScanResult.Candidate candidate = new SkillGitScanResult.Candidate(
                candidateId, root.isEmpty() ? "/" : root, name, description, status,
                resourceCount, totalBytes, issues);
        return new CandidateBuild(candidate, files);
    }

    private void validateResource(Repository repository, RawEntry entry, String relative,
                                  Set<String> collisionKeys, List<SkillGitScanResult.Issue> issues) {
        if (FileMode.SYMLINK.equals(entry.mode())) {
            issues.add(issue("GIT_SYMLINK_UNSUPPORTED", "暂不支持符号链接", relative));
            return;
        }
        if (FileMode.GITLINK.equals(entry.mode())) {
            issues.add(issue("GIT_SUBMODULE_UNSUPPORTED", "暂不支持 Git 子模块", relative));
            return;
        }
        if (!isRegular(entry.mode())) {
            issues.add(issue("GIT_OBJECT_UNSUPPORTED", "不支持该 Git 文件对象类型", relative));
            return;
        }
        String normalized;
        try {
            normalized = SkillPaths.normalize(relative);
        } catch (SkillValidationException exception) {
            issues.add(issue("UNSAFE_RESOURCE_PATH", "资源路径不符合 Skill 规范", relative));
            return;
        }
        String collisionKey = SkillPaths.collisionKey(normalized);
        if (!collisionKeys.add(collisionKey)) {
            issues.add(issue("DUPLICATE_RESOURCE_PATH", "资源路径存在大小写或 Unicode 冲突", relative));
        }
        if (normalized.length() > packageLimits.getMaxPathLength()
                || SkillPaths.depth(normalized) + 1 > packageLimits.getMaxPathDepth()) {
            issues.add(issue("RESOURCE_PATH_LIMIT", "资源路径长度或深度超过限制", relative));
        }
        String mediaType = mediaType(relative);
        boolean text = SkillResources.isText(relative, mediaType);
        long fileLimit = text ? packageLimits.getMaxTextFileBytes() : packageLimits.getMaxBinaryFileBytes();
        if (entry.size() > fileLimit) {
            issues.add(issue(text ? "TEXT_FILE_SIZE_LIMIT" : "BINARY_FILE_SIZE_LIMIT",
                    "资源文件超过允许大小", relative));
            return;
        }
        try {
            byte[] bytes = null;
            if (text) {
                bytes = loadBytes(repository, entry);
                decodeUtf8(bytes);
            }
            if (entry.size() <= LFS_POINTER_MAX_BYTES
                    && isLfsPointer(bytes == null ? loadBytes(repository, entry) : bytes)) {
                issues.add(issue("GIT_LFS_UNRESOLVED", "Git LFS 文件尚未包含真实内容", relative));
            }
        } catch (CharacterCodingException exception) {
            issues.add(issue("INVALID_UTF8", "文本资源不是有效的 UTF-8 内容", relative));
        } catch (IOException exception) {
            throw new BusinessException(500, 500, "读取 Git Skill 资源失败：" + relative, exception);
        }
    }

    private byte[] loadBytes(Repository repository, RawEntry entry) throws IOException {
        ObjectLoader loader = repository.open(entry.objectId(), Constants.OBJ_BLOB);
        long maximum = Math.max(packageLimits.getMaxBinaryFileBytes(), packageLimits.getMaxTextFileBytes());
        if (loader.getSize() > maximum) {
            throw new IOException("Git Blob exceeds the configured single-file limit");
        }
        return loader.getBytes((int) Math.min(Integer.MAX_VALUE, maximum));
    }

    private String decodeUtf8(byte[] bytes) throws CharacterCodingException {
        return StandardCharsets.UTF_8.newDecoder()
                .onMalformedInput(CodingErrorAction.REPORT)
                .onUnmappableCharacter(CodingErrorAction.REPORT)
                .decode(ByteBuffer.wrap(bytes)).toString();
    }

    private boolean isLfsPointer(byte[] bytes) {
        if (bytes.length < LFS_HEADER.length) {
            return false;
        }
        for (int index = 0; index < LFS_HEADER.length; index++) {
            if (bytes[index] != LFS_HEADER[index]) {
                return false;
            }
        }
        return true;
    }

    private SkillGitScanResult.Issue toIssue(SkillValidationIssue source) {
        return issue(source.getCode(), source.getMessage(), source.getPath());
    }

    private SkillGitScanResult.Issue issue(String code, String message, String path) {
        return new SkillGitScanResult.Issue(code, message, path);
    }

    private String mediaType(String path) {
        String guessed = URLConnection.guessContentTypeFromName(path);
        return guessed == null ? "application/octet-stream" : guessed;
    }

    private long addSize(long total, long size) {
        try {
            return Math.addExact(total, Math.max(0L, size));
        } catch (ArithmeticException exception) {
            return Long.MAX_VALUE;
        }
    }

    private boolean isRegular(FileMode mode) {
        return FileMode.REGULAR_FILE.equals(mode) || FileMode.EXECUTABLE_FILE.equals(mode);
    }

    private String relativePath(String root, String path) {
        return root.isEmpty() ? path : path.substring(root.length() + 1);
    }

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

    private String fileName(String path) {
        int index = path.lastIndexOf('/');
        return index < 0 ? path : path.substring(index + 1);
    }

    private record RawEntry(String path, ObjectId objectId, FileMode mode, long size) {
    }

    private record CandidateBuild(SkillGitScanResult.Candidate candidate,
                                  List<SkillGitRepositoryScan.RepositoryFile> files) {
    }
}
