package tech.easyflow.agent.runtime.workspace;

import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
import tech.easyflow.agent.config.AgentWorkspaceProperties;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.util.HexFormat;
import java.util.regex.Pattern;

/**
 * 按租户、Agent 和运行会话分配本地工作区，并维护服务端活动标记。
 */
@Component
public class AgentWorkspaceResolver {

    private static final Pattern SESSION_ID = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._-]{0,127}");
    private static final String ACTIVITY_DIRECTORY = ".easyflow-activity";

    private final AgentWorkspaceProperties properties;
    private Path realRoot;
    private Path activityRoot;

    /**
     * 创建工作区解析器。
     *
     * @param properties 工作区配置
     */
    public AgentWorkspaceResolver(AgentWorkspaceProperties properties) {
        this.properties = properties;
    }

    /**
     * 初始化并校验工作区根目录。
     */
    @PostConstruct
    public void initialize() {
        try {
            Path configured = Path.of(properties.getRoot()).toAbsolutePath().normalize();
            Files.createDirectories(configured);
            if (Files.isSymbolicLink(configured)) {
                throw new IllegalStateException("Agent 工作区根目录不能是符号链接");
            }
            realRoot = configured.toRealPath(LinkOption.NOFOLLOW_LINKS);
            activityRoot = realRoot.resolve(ACTIVITY_DIRECTORY);
            if (Files.exists(activityRoot, LinkOption.NOFOLLOW_LINKS) && Files.isSymbolicLink(activityRoot)) {
                throw new IllegalStateException("Agent 工作区活动目录不能是符号链接");
            }
            Files.createDirectories(activityRoot);
            if (Files.isSymbolicLink(activityRoot)) {
                throw new IllegalStateException("Agent 工作区活动目录不能是符号链接");
            }
        } catch (IOException error) {
            throw new IllegalStateException("初始化 Agent 工作区根目录失败", error);
        }
    }

    /**
     * 解析并创建一个隔离的会话工作区。
     *
     * @param tenantId 租户 ID
     * @param agentId Agent ID
     * @param runtimeSessionId Runtime 会话 ID
     * @return 经过真实路径校验的绝对工作区
     */
    public Path resolve(BigInteger tenantId, BigInteger agentId, String runtimeSessionId) {
        if (tenantId == null || tenantId.signum() <= 0 || agentId == null || agentId.signum() <= 0) {
            throw new BusinessException("Agent 工作区租户和 Agent 标识不完整");
        }
        if (runtimeSessionId == null || !SESSION_ID.matcher(runtimeSessionId).matches()) {
            throw new BusinessException("Agent 工作区会话标识不合法");
        }
        Path target = realRoot.resolve(tenantId.toString()).resolve(agentId.toString())
                .resolve(runtimeSessionId).normalize();
        if (!target.startsWith(realRoot)) {
            throw new BusinessException("Agent 工作区路径越界");
        }
        try {
            createSafeDirectories(target);
            Path realTarget = target.toRealPath(LinkOption.NOFOLLOW_LINKS);
            if (!realTarget.startsWith(realRoot) || Files.isSymbolicLink(realTarget)) {
                throw new BusinessException("Agent 工作区路径越界");
            }
            touch(realTarget);
            return realTarget;
        } catch (IOException error) {
            throw new BusinessException(500, 500, "创建 Agent 会话工作区失败", error);
        }
    }

    /**
     * 安全解析工作区内一个已经存在的普通文件。
     *
     * @param workspace 会话工作区
     * @param relativePath 模型提交的相对路径
     * @return 文件真实绝对路径
     */
    public Path resolveExistingFile(Path workspace, String relativePath) {
        if (workspace == null || relativePath == null || relativePath.isBlank()
                || relativePath.indexOf('\0') >= 0 || relativePath.startsWith("~")) {
            throw new BusinessException("WORKSPACE_PATH_FORBIDDEN: 文件路径必须是工作区相对路径");
        }
        Path raw;
        try {
            raw = Path.of(relativePath);
        } catch (InvalidPathException error) {
            throw new BusinessException("WORKSPACE_PATH_FORBIDDEN: 文件路径格式不合法");
        }
        if (raw.isAbsolute()) {
            throw new BusinessException("WORKSPACE_PATH_FORBIDDEN: 禁止绝对路径");
        }
        for (Path segment : raw) {
            if ("..".equals(segment.toString())) {
                throw new BusinessException("WORKSPACE_PATH_FORBIDDEN: 禁止路径回退");
            }
        }
        try {
            Path realWorkspace = workspace.toRealPath(LinkOption.NOFOLLOW_LINKS);
            Path candidate = realWorkspace.resolve(raw).normalize();
            if (!candidate.startsWith(realWorkspace) || !Files.exists(candidate, LinkOption.NOFOLLOW_LINKS)) {
                throw new BusinessException("WORKSPACE_FILE_NOT_FOUND: 工作区文件不存在");
            }
            rejectSymlinkChain(realWorkspace, candidate);
            Path realFile = candidate.toRealPath(LinkOption.NOFOLLOW_LINKS);
            if (!realFile.startsWith(realWorkspace) || !Files.isRegularFile(realFile, LinkOption.NOFOLLOW_LINKS)) {
                throw new BusinessException("WORKSPACE_FILE_TYPE_UNSUPPORTED: 只允许普通文件");
            }
            rejectUnixHardlink(realFile);
            touch(realWorkspace);
            return realFile;
        } catch (BusinessException error) {
            throw error;
        } catch (IOException error) {
            throw new BusinessException(500, 500, "解析工作区文件失败", error);
        }
    }

    /**
     * 更新工作区的可信活动时间。
     *
     * @param workspace 会话工作区
     */
    public void touch(Path workspace) {
        try {
            Files.writeString(activityFile(workspace), Instant.now().toString(),
                    StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
        } catch (IOException error) {
            throw new BusinessException(500, 500, "更新 Agent 工作区活动状态失败", error);
        }
    }

    /** @return 工作区真实根目录 */
    public Path getRealRoot() { return realRoot; }
    /** @return 活动标记目录 */
    public Path getActivityRoot() { return activityRoot; }

    /**
     * 取得指定工作区的活动标记文件。
     *
     * @param workspace 会话工作区
     * @return 位于模型工作区之外的标记文件
     */
    public Path activityFile(Path workspace) {
        Path relative = realRoot.relativize(workspace.toAbsolutePath().normalize());
        String digest = sha256(relative.toString());
        return activityRoot.resolve(digest + ".activity");
    }

    private void createSafeDirectories(Path target) throws IOException {
        Path current = realRoot;
        for (Path segment : realRoot.relativize(target)) {
            current = current.resolve(segment);
            if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) {
                if (Files.isSymbolicLink(current) || !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS)) {
                    throw new BusinessException("Agent 工作区路径链不是安全目录");
                }
                continue;
            }
            try {
                Files.createDirectory(current);
            } catch (java.nio.file.FileAlreadyExistsException ignored) {
                if (Files.isSymbolicLink(current) || !Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS)) {
                    throw new BusinessException("Agent 工作区路径链不是安全目录");
                }
            }
        }
    }

    private void rejectSymlinkChain(Path root, Path target) {
        Path current = root;
        for (Path segment : root.relativize(target)) {
            current = current.resolve(segment);
            if (Files.isSymbolicLink(current)) {
                throw new BusinessException("WORKSPACE_PATH_FORBIDDEN: 路径链包含符号链接");
            }
        }
    }

    private void rejectUnixHardlink(Path file) throws IOException {
        try {
            Object value = Files.getAttribute(file, "unix:nlink", LinkOption.NOFOLLOW_LINKS);
            if (value instanceof Number number && number.longValue() > 1L) {
                throw new BusinessException("WORKSPACE_FILE_TYPE_UNSUPPORTED: 禁止发布硬链接文件");
            }
        } catch (UnsupportedOperationException ignored) {
            // 非 Unix 文件系统没有 nlink 属性，仍保留普通文件与符号链接校验。
        }
    }

    private String sha256(String value) {
        try {
            return HexFormat.of().formatHex(
                    java.security.MessageDigest.getInstance("SHA-256")
                            .digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8)));
        } catch (java.security.NoSuchAlgorithmException error) {
            throw new IllegalStateException("当前 JVM 不支持 SHA-256", error);
        }
    }
}
