package tech.easyflow.common.filestorage.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.common.filestorage.FileStorageWriteHandle;
import tech.easyflow.common.filestorage.FileStorageWriteResult;
import tech.easyflow.common.filestorage.utils.PathGeneratorUtil;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;

/**
 * EasyFlow 本地文件存储实现。
 */
@Component("local")
public class LocalFileStorageServiceImpl implements FileStorageService {
    /** 日志记录器。 */
    private static final Logger LOG = LoggerFactory.getLogger(LocalFileStorageServiceImpl.class);
    /** 可恢复句柄使用的后端路由名称。 */
    private static final String RECOVERABLE_BACKEND = "local";

    /** 本地存储根目录。 */
    @Value("${easyflow.storage.local.root:}")
    private String root;
    /** 返回给旧读取链路的 URL 前缀。 */
    @Value("${easyflow.storage.local.prefix:}")
    private String prefix;

    /**
     * 应用启动后的本地存储初始化钩子。
     */
    @EventListener(ApplicationReadyEvent.class)
    public void init() {
    }

    /**
     * 使用随机用户路径保存文件。
     *
     * @param file 上传文件
     * @return 文件路径
     */
    @Override
    public String save(MultipartFile file) {
        try {
            String path = PathGeneratorUtil.generateUserPath(file.getOriginalFilename());
            File target = getLocalFile(path);
            if (!target.getParentFile().exists() && !target.getParentFile().mkdirs()) {
                LOG.error("创建文件失败: {} ", target.getParentFile());
            }
            file.transferTo(target);
            return prefix + path;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 打开本地文件读取流。
     *
     * @param path 文件路径
     * @return 文件输入流
     * @throws IOException 文件不存在或不可读时抛出
     */
    @Override
    public InputStream readStream(String path) throws IOException {
        File target = getLocalFile(path);
        return Files.newInputStream(target.toPath());
    }

    /**
     * 获取本地文件大小。
     *
     * @param path 文件路径
     * @return 文件大小，不存在时返回 0
     */
    @Override
    public long getFileSize(String path) {
        File target;
        try {
            target = getLocalFile(path);
        } catch (IOException e) {
            throw new RuntimeException("获取文件大小出错", e);
        }
        if (target.exists()) {
            return target.length();
        }
        return 0;
    }

    /**
     * 幂等删除旧版路径对应的本地文件。
     *
     * @param path 文件路径
     */
    @Override
    public void delete(String path) {
        try {
            File file = getLocalFile(path);
            Files.deleteIfExists(file.toPath());
        } catch (IOException e) {
            LOG.error("删除本地文件出错: {}", path, e);
            throw new RuntimeException("删除本地文件出错：",e);
        }
    }

    /**
     * 递归删除文件或目录（支持删除非空目录）
     *
     * @param file 要删除的文件或目录
     * @throws Exception 任一目标无法删除时抛出
     */
    private void deleteRecursively(File file) throws Exception {
        if (file == null || !file.exists()) {
            LOG.warn("文件/目录不存在: {}", file);
            return;
        }

        // 如果是目录，先递归删除子文件和子目录
        if (file.isDirectory()) {
            File[] children = file.listFiles();
            if (children != null) { // 防止null（目录可能被其他进程修改）
                for (File child : children) {
                    deleteRecursively(child); // 递归删除子内容
                }
            }
        }

        // 删除当前文件或空目录
        boolean deleted = file.delete();
        if (!deleted) {
            throw new Exception("无法删除文件/目录: " + file.getAbsolutePath());
        }
    }

    /**
     * 将旧版 URL 转换为本地文件。
     *
     * @param path 文件 URL 或路径
     * @return 本地文件
     * @throws IOException 路径转换失败时抛出
     */
    private File getLocalFile(String path) throws IOException {
        if (this.root == null || this.root.isEmpty()) {
            throw new RuntimeException("请指定存储根目录");
        }
        return new File(this.root, path.replace(prefix, ""));
    }

    /**
     * 使用指定前置目录与随机用户路径保存文件。
     *
     * @param file 上传文件
     * @param prePath 前置目录
     * @return 文件路径
     */
    @Override
    public String save(MultipartFile file, String prePath) {
        try {
            String path = PathGeneratorUtil.generateUserPath(file.getOriginalFilename());
            if (StringUtils.hasText(prePath)) {
                String normalized = prePath.replaceAll("^/+", "").replaceAll("/+$", "");
                path = normalized + "/" + path.replaceAll("^/+", "");
            }
            File target = getLocalFile(path);
            if (!target.getParentFile().exists() && !target.getParentFile().mkdirs()) {
                LOG.error("创建文件失败: {} ", target.getParentFile());
            }
            file.transferTo(target);
            return prefix + path;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    /**
     * 准备包含真实、稳定本地根目录的可恢复写句柄。
     *
     * @param path 根目录下的相对目录
     * @param filename 固定文件名
     * @return 本地可恢复写句柄
     */
    @Override
    public FileStorageWriteHandle prepareRecoverableWrite(String path, String filename) {
        try {
            Path stableRoot = prepareStableRoot();
            return new FileStorageWriteHandle(
                    RECOVERABLE_BACKEND, "", stableRoot.toString(), path, filename);
        } catch (IOException exception) {
            throw new IllegalStateException("准备本地可恢复文件写入失败", exception);
        }
    }

    /**
     * 通过同目录临时文件及原子替换写入句柄指定的精确本地文件。
     *
     * @param file 上传文件
     * @param handle 本地可恢复写句柄
     * @return 本地读取 URL 与恢复 locator
     * @throws RuntimeException 写入、刷盘、原子替换或结果确认失败时抛出
     */
    @Override
    public FileStorageWriteResult saveRecoverable(MultipartFile file, FileStorageWriteHandle handle) {
        if (file == null) {
            throw new IllegalArgumentException("上传文件不能为空");
        }
        requireLocalHandle(handle);
        Path temporary = null;
        try {
            Path target = resolveControlledTarget(handle, true);
            if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)
                    && (Files.isSymbolicLink(target) || !Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS))) {
                throw new IllegalStateException("本地可恢复写入目标不是普通文件: " + target);
            }
            temporary = recoverablePartPath(target, handle);
            if (Files.exists(temporary, LinkOption.NOFOLLOW_LINKS)
                    && (Files.isSymbolicLink(temporary)
                    || !Files.isRegularFile(temporary, LinkOption.NOFOLLOW_LINKS))) {
                throw new IllegalStateException("本地可恢复写入暂存目标不是普通文件: " + temporary);
            }
            try (InputStream input = file.getInputStream();
                 FileChannel channel = FileChannel.open(
                         temporary, StandardOpenOption.CREATE, StandardOpenOption.WRITE,
                         StandardOpenOption.TRUNCATE_EXISTING)) {
                OutputStream output = Channels.newOutputStream(channel);
                input.transferTo(output);
                channel.force(true);
            }
            try {
                Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
            } catch (AtomicMoveNotSupportedException exception) {
                throw new IllegalStateException("本地文件系统不支持可恢复写入所需的原子替换", exception);
            }
            temporary = null;
            if (!Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(target)) {
                throw new IllegalStateException("本地可恢复写入后未找到普通物理文件: " + target);
            }
            String objectPath = handle.getPath() + handle.getFilename();
            String url = StringUtils.hasText(prefix)
                    ? (prefix.endsWith("/") ? prefix : prefix + "/") + objectPath
                    : objectPath;
            return new FileStorageWriteResult(url, handle.encodeLocator());
        } catch (IOException exception) {
            throw new IllegalStateException("写入本地可恢复文件失败", exception);
        } finally {
            if (temporary != null) {
                try {
                    Files.deleteIfExists(temporary);
                } catch (IOException cleanupException) {
                    LOG.warn("清理本地可恢复写入临时文件失败: {}", temporary, cleanupException);
                }
            }
        }
    }

    /**
     * 按句柄固化的本地根目录精确打开普通文件。
     *
     * @param handle 本地可恢复写句柄
     * @return 文件输入流，由调用方关闭
     * @throws IOException 文件不存在、不可读或路径不安全时抛出
     */
    @Override
    public InputStream readRecoverable(FileStorageWriteHandle handle) throws IOException {
        requireLocalHandle(handle);
        Path target = resolveControlledTarget(handle, false);
        if (!Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
            throw new IOException("本地可恢复文件不存在: " + target);
        }
        if (Files.isSymbolicLink(target)
                || !Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS)) {
            throw new IOException("本地可恢复读取目标不是普通文件: " + target);
        }
        return Files.newInputStream(target);
    }

    /**
     * 精确且幂等地删除句柄对应的最终文件与确定性暂存文件，并确认两者均不存在。
     *
     * @param handle 本地可恢复写句柄
     * @throws RuntimeException 目标不安全、删除失败或删除后仍存在时抛出
     */
    @Override
    public void deleteRecoverable(FileStorageWriteHandle handle) {
        requireLocalHandle(handle);
        try {
            Path target = resolveControlledTarget(handle, false);
            Path temporary = recoverablePartPath(target, handle);
            deleteControlledRegularFile(target, "最终文件");
            deleteControlledRegularFile(temporary, "暂存文件");
        } catch (IOException exception) {
            throw new IllegalStateException("删除本地可恢复文件失败", exception);
        }
    }

    /**
     * 精确检查句柄对应的本地普通文件是否存在。
     *
     * @param handle 本地可恢复写句柄
     * @return 普通物理文件存在时返回 true
     * @throws RuntimeException 路径包含符号链接或目标不是普通文件时抛出
     */
    @Override
    public boolean existsRecoverable(FileStorageWriteHandle handle) {
        requireLocalHandle(handle);
        try {
            Path target = resolveControlledTarget(handle, false);
            if (!Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
                return false;
            }
            if (Files.isSymbolicLink(target) || !Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS)) {
                throw new IllegalStateException("本地恢复目标不是普通文件: " + target);
            }
            return true;
        } catch (IOException exception) {
            throw new IllegalStateException("检查本地可恢复文件失败", exception);
        }
    }

    /**
     * 创建并解析配置根目录的真实路径，使句柄不依赖符号链接及后续配置切换。
     *
     * @return 已存在的真实根目录
     * @throws IOException 无法创建或解析根目录时抛出
     */
    private Path prepareStableRoot() throws IOException {
        if (!StringUtils.hasText(root)) {
            throw new IllegalStateException("请指定存储根目录");
        }
        Path configuredRoot = Path.of(root).toAbsolutePath().normalize();
        Files.createDirectories(configuredRoot);
        Path realRoot = configuredRoot.toRealPath();
        if (!Files.isDirectory(realRoot, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(realRoot)) {
            throw new IllegalStateException("本地存储根目录不是受控普通目录: " + configuredRoot);
        }
        return realRoot;
    }

    /**
     * 在句柄固化根目录下解析目标，并逐级拒绝符号链接与路径逃逸。
     *
     * @param handle 本地可恢复写句柄
     * @param createDirectories 是否创建缺失目录
     * @return 受控目标文件路径
     * @throws IOException 路径检查或目录创建失败时抛出
     */
    private Path resolveControlledTarget(FileStorageWriteHandle handle, boolean createDirectories) throws IOException {
        Path stableRoot = Path.of(handle.getBasePath());
        if (!stableRoot.isAbsolute() || !stableRoot.normalize().equals(stableRoot)) {
            throw new IllegalArgumentException("本地恢复句柄中的根目录不是规范绝对路径");
        }
        Path expectedTarget = stableRoot.resolve(handle.getPath()).resolve(handle.getFilename()).normalize();
        if (!expectedTarget.startsWith(stableRoot) || expectedTarget.getParent() == null
                || !expectedTarget.getParent().startsWith(stableRoot)) {
            throw new IllegalArgumentException("本地恢复目标逃逸存储根目录");
        }
        if (!Files.exists(stableRoot, LinkOption.NOFOLLOW_LINKS)) {
            if (!createDirectories) {
                return expectedTarget;
            }
            Files.createDirectories(stableRoot);
        }
        if (Files.isSymbolicLink(stableRoot) || !Files.isDirectory(stableRoot, LinkOption.NOFOLLOW_LINKS)
                || !stableRoot.toRealPath().equals(stableRoot)) {
            throw new IllegalStateException("本地恢复句柄根目录不再是原受控目录: " + stableRoot);
        }

        Path parent = stableRoot;
        if (!handle.getPath().isEmpty()) {
            String relativeDirectory = handle.getPath().substring(0, handle.getPath().length() - 1);
            for (String segment : relativeDirectory.split("/")) {
                Path next = parent.resolve(segment);
                if (Files.exists(next, LinkOption.NOFOLLOW_LINKS)) {
                    if (Files.isSymbolicLink(next) || !Files.isDirectory(next, LinkOption.NOFOLLOW_LINKS)) {
                        throw new IllegalStateException("本地恢复路径包含非普通目录: " + next);
                    }
                } else if (createDirectories) {
                    Files.createDirectory(next);
                } else {
                    return expectedTarget;
                }
                parent = next;
            }
        }
        if (!parent.toRealPath().equals(parent)) {
            throw new IllegalStateException("本地恢复目标父目录已逃逸受控路径: " + parent);
        }
        return expectedTarget;
    }

    /**
     * 根据句柄稳定推导同目录暂存文件，确保进程在原子替换前退出时仍可精确回收。
     *
     * @param target 最终目标文件
     * @param handle 可恢复写句柄
     * @return 确定性同目录暂存文件
     */
    Path recoverablePartPath(Path target, FileStorageWriteHandle handle) {
        try {
            byte[] digest = MessageDigest.getInstance("SHA-256")
                    .digest(handle.encodeLocator().getBytes(java.nio.charset.StandardCharsets.UTF_8));
            return target.resolveSibling(".easyflow-part-" + HexFormat.of().formatHex(digest));
        } catch (NoSuchAlgorithmException exception) {
            throw new IllegalStateException("当前 JVM 不支持 SHA-256", exception);
        }
    }

    /**
     * 删除受控普通文件并确认不存在；文件原本不存在时按幂等成功处理。
     *
     * @param path 待删除文件
     * @param description 文件用途描述
     * @throws IOException 删除失败时抛出
     */
    private void deleteControlledRegularFile(Path path, String description) throws IOException {
        if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
            return;
        }
        if (Files.isSymbolicLink(path) || !Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
            throw new IllegalStateException("拒绝删除非普通的本地恢复" + description + ": " + path);
        }
        Files.delete(path);
        if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
            throw new IllegalStateException("删除后本地恢复" + description + "仍存在: " + path);
        }
    }

    /**
     * 校验句柄确实属于本地后端。
     *
     * @param handle 待校验句柄
     */
    private void requireLocalHandle(FileStorageWriteHandle handle) {
        if (handle == null) {
            throw new IllegalArgumentException("本地文件存储写句柄不能为空");
        }
        if (!RECOVERABLE_BACKEND.equals(handle.getBackend()) || !handle.getPlatform().isEmpty()) {
            throw new IllegalArgumentException("文件存储写句柄不属于本地后端");
        }
    }
}
