package com.easyagents.skill.store.file;

import com.easyagents.skill.exception.SkillException;
import com.easyagents.skill.store.SkillContentStage;
import com.easyagents.skill.store.SkillContentStore;
import com.easyagents.skill.util.SkillHashes;

import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.Cleaner;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * 基于进程临时目录的流式 Skill 内容存储。
 *
 * <p>内容先写入隔离暂存文件，提交后按 SHA-256 去重，并由引用计数控制物理文件生命周期。
 * 调用方应在不再使用内容引用和已打开的输入流后调用 {@link #close()}。未显式关闭的实例会由
 * JVM Cleaner 尝试兜底清理，但 Cleaner 不应替代正常的生命周期管理。</p>
 */
public final class TemporaryFileSkillContentStore implements SkillContentStore, AutoCloseable {

    private static final String DIRECTORY_PREFIX = "easy-agents-skill-content-";
    private static final int COPY_BUFFER_SIZE = 16 * 1024;
    private static final Cleaner CLEANER = Cleaner.create();

    private final Object lifecycleMonitor = new Object();
    private final Path storageDirectory;
    private final Path stagedDirectory;
    private final Path contentDirectory;
    private final Map<String, StagedContent> stagedContents = new HashMap<>();
    private final Map<String, StoredContent> contents = new HashMap<>();
    private final Cleaner.Cleanable cleanable;
    private volatile boolean closed;

    /**
     * 在系统临时目录中创建独立内容存储。
     */
    public TemporaryFileSkillContentStore() {
        this(null);
    }

    /**
     * 在指定父目录中创建独立内容存储。
     *
     * <p>该实例只拥有新建的子目录，关闭时不会删除传入的父目录。</p>
     *
     * @param parentDirectory 临时存储父目录；为 null 时使用系统临时目录
     */
    public TemporaryFileSkillContentStore(Path parentDirectory) {
        this.storageDirectory = createStorageDirectory(parentDirectory);
        this.stagedDirectory = storageDirectory.resolve("staged");
        this.contentDirectory = storageDirectory.resolve("content");
        try {
            Files.createDirectory(stagedDirectory);
            Files.createDirectory(contentDirectory);
        } catch (IOException exception) {
            deleteDirectoryQuietly(storageDirectory);
            throw new SkillException("Failed to initialize temporary Skill content store.", exception);
        }
        this.cleanable = CLEANER.register(this, new DirectoryCleanup(storageDirectory));
    }

    /**
     * 流式保存字节内容并持有一个正式引用。
     *
     * @param bytes 内容字节；null 按空内容处理
     * @return SHA-256 内容引用
     */
    @Override
    public String put(byte[] bytes) {
        byte[] safeBytes = bytes == null ? new byte[0] : bytes;
        SkillContentStage stage = stage(new ByteArrayInputStream(safeBytes), safeBytes.length);
        try {
            return commit(stage);
        } catch (RuntimeException exception) {
            try {
                rollback(stage);
            } catch (RuntimeException cleanupError) {
                exception.addSuppressed(cleanupError);
            }
            throw exception;
        }
    }

    /**
     * 将内容流式写入独立暂存文件并同步计算 SHA-256。
     *
     * @param inputStream 内容流，不由本方法关闭
     * @param maxBytes 最大允许字节数
     * @return 暂存结果
     */
    @Override
    public SkillContentStage stage(InputStream inputStream, long maxBytes) {
        if (inputStream == null || maxBytes < 0) {
            throw new SkillException("Valid Skill content stream and limit are required.");
        }
        String stageId = UUID.randomUUID().toString();
        Path stagedPath = stagedDirectory.resolve(stageId + ".stage");
        ensureOpen();
        try {
            MessageDigest digest = SkillHashes.newSha256Digest();
            long size = 0;
            try (DigestOutputStream output = new DigestOutputStream(
                    Files.newOutputStream(stagedPath, StandardOpenOption.CREATE_NEW,
                            StandardOpenOption.WRITE), digest)) {
                byte[] buffer = new byte[COPY_BUFFER_SIZE];
                int length;
                while ((length = inputStream.read(buffer)) >= 0) {
                    if (length == 0) {
                        continue;
                    }
                    if (size > maxBytes - length) {
                        throw new SkillException("Skill content exceeds " + maxBytes + " bytes.");
                    }
                    output.write(buffer, 0, length);
                    size += length;
                }
            }
            String contentHash = SkillHashes.toHex(digest.digest());
            String contentRef = "sha256:" + contentHash;
            StagedContent stagedContent = new StagedContent(
                    stagedPath, contentRef, contentHash, size);
            synchronized (lifecycleMonitor) {
                ensureOpenLocked();
                stagedContents.put(stageId, stagedContent);
            }
            return new SkillContentStage(stageId, contentRef, contentHash, size);
        } catch (IOException | RuntimeException exception) {
            deleteFileQuietly(stagedPath);
            if (exception instanceof SkillException skillException) {
                throw skillException;
            }
            throw new SkillException("Failed to stage temporary Skill content.", exception);
        }
    }

    /**
     * 原子地提交暂存内容并增加一份正式引用。
     *
     * @param stage 暂存结果
     * @return SHA-256 内容引用
     */
    @Override
    public String commit(SkillContentStage stage) {
        if (stage == null) {
            throw new SkillException("Skill content stage is required.");
        }
        synchronized (lifecycleMonitor) {
            ensureOpenLocked();
            StagedContent stagedContent = stagedContents.get(stage.getStageId());
            validateStage(stage, stagedContent);
            validateStagedFile(stagedContent);

            StoredContent existing = contents.get(stagedContent.contentRef);
            if (existing != null) {
                validateDuplicateContent(stagedContent, existing);
                if (existing.references == Long.MAX_VALUE) {
                    throw new SkillException("Skill content reference count overflow: "
                            + stagedContent.contentRef);
                }
                deleteFile(stagedContent.path, "Failed to delete committed Skill stage.");
                stagedContents.remove(stage.getStageId());
                existing.references++;
                existing.pendingDeletion = false;
                return stagedContent.contentRef;
            }

            Path targetPath = contentDirectory.resolve(stagedContent.contentHash + ".content");
            if (Files.exists(targetPath, LinkOption.NOFOLLOW_LINKS)) {
                throw new SkillException("Unexpected Skill content file already exists: "
                        + stagedContent.contentRef);
            }
            moveAtomically(stagedContent.path, targetPath);
            contents.put(stagedContent.contentRef,
                    new StoredContent(targetPath, stagedContent.size));
            stagedContents.remove(stage.getStageId());
            return stagedContent.contentRef;
        }
    }

    /**
     * 删除尚未提交的暂存内容。
     *
     * @param stage 暂存结果
     */
    @Override
    public void rollback(SkillContentStage stage) {
        if (stage == null) {
            return;
        }
        synchronized (lifecycleMonitor) {
            if (closed) {
                return;
            }
            StagedContent stagedContent = stagedContents.get(stage.getStageId());
            if (stagedContent == null) {
                return;
            }
            validateStage(stage, stagedContent);
            deleteFile(stagedContent.path, "Failed to rollback staged Skill content.");
            stagedContents.remove(stage.getStageId());
        }
    }

    /**
     * 增加一份正式内容引用。
     *
     * @param contentRef 内容引用
     */
    @Override
    public void retain(String contentRef) {
        synchronized (lifecycleMonitor) {
            ensureOpenLocked();
            StoredContent content = contents.get(contentRef);
            if (content == null || content.references <= 0) {
                throw new SkillException("Skill content does not exist: " + contentRef);
            }
            if (content.references == Long.MAX_VALUE) {
                throw new SkillException("Skill content reference count overflow: " + contentRef);
            }
            content.references++;
        }
    }

    /**
     * 释放一份正式内容引用，最后一个引用释放后删除物理文件。
     *
     * @param contentRef 内容引用
     */
    @Override
    public void release(String contentRef) {
        if (contentRef == null || contentRef.isBlank()) {
            return;
        }
        synchronized (lifecycleMonitor) {
            if (closed) {
                return;
            }
            StoredContent content = contents.get(contentRef);
            if (content == null) {
                return;
            }
            if (content.references > 0) {
                content.references--;
            }
            if (content.references == 0) {
                content.pendingDeletion = true;
                deleteReleasedContent(contentRef, content);
            }
        }
    }

    /**
     * 打开正式内容的文件流。
     *
     * <p>返回流会持有读取租约，最后一个正式引用释放时会等待已打开的流关闭后再删除文件。</p>
     *
     * @param contentRef 内容引用
     * @return 内容输入流，调用方负责关闭
     */
    @Override
    public InputStream open(String contentRef) {
        synchronized (lifecycleMonitor) {
            ensureOpenLocked();
            StoredContent content = contents.get(contentRef);
            if (content == null || content.references <= 0) {
                throw new SkillException("Skill content does not exist: " + contentRef);
            }
            if (content.readers == Integer.MAX_VALUE) {
                throw new SkillException("Skill content reader count overflow: " + contentRef);
            }
            try {
                InputStream input = Files.newInputStream(content.path, StandardOpenOption.READ);
                content.readers++;
                return new LeasedInputStream(input, contentRef, content);
            } catch (IOException exception) {
                throw new SkillException("Failed to open temporary Skill content: " + contentRef,
                        exception);
            }
        }
    }

    /**
     * 判断正式内容是否仍有有效引用。
     *
     * @param contentRef 内容引用
     * @return 存在且至少持有一个引用时为 true
     */
    @Override
    public boolean exists(String contentRef) {
        synchronized (lifecycleMonitor) {
            if (closed) {
                return false;
            }
            StoredContent content = contents.get(contentRef);
            return content != null && content.references > 0
                    && Files.isRegularFile(content.path, LinkOption.NOFOLLOW_LINKS);
        }
    }

    /**
     * 清理全部暂存和正式内容并关闭存储。
     *
     * <p>调用前应先关闭通过 {@link #open(String)} 获取的流。该方法可重复调用。</p>
     */
    @Override
    public void close() {
        synchronized (lifecycleMonitor) {
            if (closed) {
                return;
            }
            closed = true;
            stagedContents.clear();
            contents.clear();
        }
        try {
            deleteDirectory(storageDirectory);
        } catch (IOException exception) {
            throw new SkillException("Failed to clean temporary Skill content store.", exception);
        } finally {
            cleanable.clean();
        }
    }

    /**
     * 返回该实例拥有的临时目录，供同包生命周期测试使用。
     *
     * @return 实例临时目录
     */
    Path storageDirectory() {
        return storageDirectory;
    }

    private void ensureOpen() {
        synchronized (lifecycleMonitor) {
            ensureOpenLocked();
        }
    }

    private void ensureOpenLocked() {
        if (closed) {
            throw new SkillException("Temporary Skill content store is closed.");
        }
    }

    private static void validateStage(SkillContentStage stage, StagedContent content) {
        if (content == null) {
            throw new SkillException("Skill content stage does not exist: " + stage.getStageId());
        }
        if (!content.contentRef.equals(stage.getContentRef())
                || !content.contentHash.equals(stage.getContentHash())
                || content.size != stage.getSize()) {
            throw new SkillException("Skill content stage metadata does not match: "
                    + stage.getStageId());
        }
    }

    private static void validateStagedFile(StagedContent content) {
        try {
            BasicFileAttributes attributes = Files.readAttributes(
                    content.path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
            if (!attributes.isRegularFile() || attributes.size() != content.size) {
                throw new SkillException("Staged Skill content file changed before commit: "
                        + content.contentRef);
            }
        } catch (IOException exception) {
            throw new SkillException("Failed to inspect staged Skill content: "
                    + content.contentRef, exception);
        }
    }

    private static void validateDuplicateContent(StagedContent stagedContent,
                                                 StoredContent existing) {
        if (existing.size != stagedContent.size) {
            throw new SkillException("SHA-256 collision detected for Skill content: "
                    + stagedContent.contentRef);
        }
        try {
            if (Files.mismatch(stagedContent.path, existing.path) != -1) {
                throw new SkillException("SHA-256 collision detected for Skill content: "
                        + stagedContent.contentRef);
            }
        } catch (IOException exception) {
            throw new SkillException("Failed to compare duplicate Skill content: "
                    + stagedContent.contentRef, exception);
        }
    }

    private static void moveAtomically(Path source, Path target) {
        try {
            Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
        } catch (AtomicMoveNotSupportedException exception) {
            try {
                Files.move(source, target);
            } catch (IOException fallbackException) {
                fallbackException.addSuppressed(exception);
                throw new SkillException("Failed to commit staged Skill content.",
                        fallbackException);
            }
        } catch (IOException exception) {
            throw new SkillException("Failed to commit staged Skill content.", exception);
        }
    }

    private void deleteReleasedContent(String contentRef, StoredContent content) {
        if (content.readers > 0) {
            return;
        }
        deleteFile(content.path, "Failed to delete released Skill content.");
        contents.remove(contentRef, content);
    }

    private void releaseReader(String contentRef, StoredContent content) throws IOException {
        synchronized (lifecycleMonitor) {
            if (content.readers > 0) {
                content.readers--;
            }
            if (closed || !content.pendingDeletion || content.readers > 0) {
                return;
            }
            try {
                deleteReleasedContent(contentRef, content);
            } catch (SkillException exception) {
                throw new IOException("Failed to delete released Skill content.", exception);
            }
        }
    }

    private static Path createStorageDirectory(Path parentDirectory) {
        Path directory = null;
        try {
            directory = parentDirectory == null
                    ? Files.createTempDirectory(DIRECTORY_PREFIX)
                    : Files.createTempDirectory(parentDirectory, DIRECTORY_PREFIX);
            restrictOwnerAccess(directory);
            return directory;
        } catch (IOException exception) {
            deleteDirectoryQuietly(directory);
            throw new SkillException("Failed to create temporary Skill content store.", exception);
        }
    }

    private static void restrictOwnerAccess(Path directory) throws IOException {
        try {
            Files.setPosixFilePermissions(directory, EnumSet.of(
                    PosixFilePermission.OWNER_READ,
                    PosixFilePermission.OWNER_WRITE,
                    PosixFilePermission.OWNER_EXECUTE));
        } catch (UnsupportedOperationException ignored) {
            // 非 POSIX 文件系统使用平台默认的临时目录访问控制。
        }
    }

    private static void deleteFile(Path path, String message) {
        try {
            Files.deleteIfExists(path);
        } catch (IOException exception) {
            throw new SkillException(message, exception);
        }
    }

    private static void deleteFileQuietly(Path path) {
        try {
            Files.deleteIfExists(path);
        } catch (IOException ignored) {
            // 后续 close 或 Cleaner 会再次清理实例目录。
        }
    }

    private static void deleteDirectory(Path directory) throws IOException {
        if (!Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) {
            return;
        }
        Files.walkFileTree(directory, new SimpleFileVisitor<>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attributes)
                    throws IOException {
                Files.deleteIfExists(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path current, IOException exception)
                    throws IOException {
                if (exception != null) {
                    throw exception;
                }
                Files.deleteIfExists(current);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    private static void deleteDirectoryQuietly(Path directory) {
        if (directory == null) {
            return;
        }
        try {
            deleteDirectory(directory);
        } catch (IOException ignored) {
            // Cleaner 是兜底路径，无法向已回收实例传播清理失败。
        }
    }

    private final class LeasedInputStream extends FilterInputStream {

        private final String contentRef;
        private final StoredContent content;
        private boolean streamClosed;

        private LeasedInputStream(InputStream input, String contentRef, StoredContent content) {
            super(input);
            this.contentRef = contentRef;
            this.content = content;
        }

        @Override
        public void close() throws IOException {
            if (streamClosed) {
                return;
            }
            streamClosed = true;
            IOException closeFailure = null;
            try {
                super.close();
            } catch (IOException exception) {
                closeFailure = exception;
            }
            try {
                releaseReader(contentRef, content);
            } catch (IOException cleanupFailure) {
                if (closeFailure == null) {
                    closeFailure = cleanupFailure;
                } else {
                    closeFailure.addSuppressed(cleanupFailure);
                }
            }
            if (closeFailure != null) {
                throw closeFailure;
            }
        }
    }

    private static final class StagedContent {

        private final Path path;
        private final String contentRef;
        private final String contentHash;
        private final long size;

        private StagedContent(Path path, String contentRef, String contentHash, long size) {
            this.path = path;
            this.contentRef = contentRef;
            this.contentHash = contentHash;
            this.size = size;
        }
    }

    private static final class StoredContent {

        private final Path path;
        private final long size;
        private long references = 1;
        private int readers;
        private boolean pendingDeletion;

        private StoredContent(Path path, long size) {
            this.path = path;
            this.size = size;
        }
    }

    private static final class DirectoryCleanup implements Runnable {

        private final Path directory;

        private DirectoryCleanup(Path directory) {
            this.directory = directory;
        }

        @Override
        public void run() {
            deleteDirectoryQuietly(directory);
        }
    }
}
