package tech.easyflow.common.filestorage.impl;

import io.minio.GetObjectArgs;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.platform.FileStorage;
import org.dromara.x.file.storage.core.platform.MinioFileStorage;
import org.dromara.x.file.storage.core.recorder.FileRecorder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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 tech.easyflow.common.util.OkHttpUtil;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;

/**
 * 基于 x-file-storage 的 EasyFlow 文件存储实现。
 */
@Component("xFileStorage")
public class XFIleStorageServiceImpl implements FileStorageService {

    /** 日志记录器。 */
    private static final Logger LOG = LoggerFactory.getLogger(XFIleStorageServiceImpl.class);
    /** 可恢复句柄使用的后端路由名称。 */
    private static final String RECOVERABLE_BACKEND = "xFileStorage";

    /** x-file-storage 聚合服务。 */
    @Autowired
    private org.dromara.x.file.storage.core.FileStorageService fileStorageService;

    /**
     * 使用默认目录上传文件。
     *
     * @param file 上传文件
     * @return 文件 URL
     */
    @Override
    public String save(MultipartFile file) {
        return save(file, null);
    }

    /**
     * 使用指定前置目录上传文件。
     *
     * @param file 上传文件
     * @param prePath 前置目录
     * @return 文件 URL
     */
    @Override
    public String save(MultipartFile file, String prePath) {
        String uploadPath = PathGeneratorUtil.generateUserPath("");
        if (StringUtils.hasText(prePath)) {
            String normalized = prePath.replaceAll("^/+", "").replaceAll("/+$", "");
            uploadPath = "/" + normalized + uploadPath;
        }
        FileInfo fileInfo = fileStorageService.of(file)
                .setPath(uploadPath)
                .setSaveFilename(file.getOriginalFilename())
                .setContentType(getFileContentType(file))
                .upload();
        if (fileInfo == null || !StringUtils.hasText(fileInfo.getUrl())) {
            throw new RuntimeException("文件上传失败");
        }
        return fileInfo.getUrl();
    }

    /**
     * 幂等删除指定文件；物理文件已不存在时同步清理残留记录。
     *
     * @param path 文件路径
     * @throws RuntimeException 文件仍存在或残留记录无法清理时抛出
     */
    @Override
    public void delete(String path) {
        boolean deleted = fileStorageService.delete(path);
        if (deleted) {
            return;
        }
        if (fileStorageService.exists(path)) {
            throw new RuntimeException("删除文件失败，物理文件仍存在，path=" + path);
        }
        org.dromara.x.file.storage.core.recorder.FileRecorder recorder = fileStorageService.getFileRecorder();
        boolean recordDeleted = recorder != null && recorder.delete(path);
        if (!recordDeleted && fileStorageService.getFileInfoByUrl(path) != null) {
            throw new RuntimeException("物理文件已删除，但文件记录清理失败，path=" + path);
        }
    }

    /**
     * 通过文件 URL 打开远程读取流。
     *
     * @param fileUrl 文件 URL
     * @return 远程输入流
     */
    @Override
    public InputStream readStream(String fileUrl) {
        return OkHttpUtil.getInputStream(fileUrl);
    }

    /**
     * 获取S3中文件的大小（单位：字节）
     * @param path 文件路径
     * @return 文件大小（字节）
     */
    @Override
    public long getFileSize(String path) {
        try {
            return OkHttpUtil.getFileSize(path);
        } catch (Exception e) {
            LOG.error("获取文件大小失败", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取上传文件的 Content-Type，并在客户端未声明时为文本文件补充 UTF-8 编码。
     *
     * @param file 上传文件
     * @return 文件媒体类型
     */
    public static String getFileContentType(MultipartFile file) {
        String originalFilename = file.getOriginalFilename();
        String contentType = file.getContentType();
        if (StringUtils.hasText(contentType)) {
            return contentType;
        }
        if (StringUtils.endsWithIgnoreCase(
                originalFilename,
                ".txt")) {
            return "text/plain; charset=utf-8";
        }
        return null;
    }

    /**
     * 从当前默认 x-file-storage 平台解析平台名与公开基础路径，准备可恢复写句柄。
     *
     * @param path 平台基础路径下的相对目录
     * @param filename 固定文件名
     * @return x-file-storage 可恢复写句柄
     * @throws RuntimeException 默认平台不存在或平台未公开 getBasePath 时抛出
     */
    @Override
    public FileStorageWriteHandle prepareRecoverableWrite(String path, String filename) {
        FileStorage storage = fileStorageService.getFileStorage();
        if (storage == null || !StringUtils.hasText(storage.getPlatform())) {
            throw new IllegalStateException("x-file-storage 默认平台不可用");
        }
        String basePath = readRequiredBasePath(storage);
        return new FileStorageWriteHandle(
                RECOVERABLE_BACKEND, storage.getPlatform(), basePath, path, filename);
    }

    /**
     * 使用句柄中的固定平台、路径及文件名上传文件。
     *
     * @param file 上传文件
     * @param handle x-file-storage 可恢复写句柄
     * @return 文件 URL 与恢复 locator
     * @throws RuntimeException 平台配置漂移、上传失败或实际位置不一致时抛出
     */
    @Override
    public FileStorageWriteResult saveRecoverable(MultipartFile file, FileStorageWriteHandle handle) {
        if (file == null) {
            throw new IllegalArgumentException("上传文件不能为空");
        }
        FileStorage storage = requireStorage(handle);
        requireCurrentBasePathForWrite(storage, handle);
        boolean physicalWriteMayHaveStarted = false;
        try {
            org.dromara.x.file.storage.core.upload.UploadPretreatment upload = fileStorageService.of(file)
                    .setPlatform(handle.getPlatform())
                    .setPath(physicalPath(handle))
                    .setSaveFilename(handle.getFilename())
                    .setContentType(getFileContentType(file));
            physicalWriteMayHaveStarted = true;
            FileInfo fileInfo = upload.upload();
            if (fileInfo == null || !StringUtils.hasText(fileInfo.getUrl())) {
                throw new IllegalStateException("x-file-storage 未返回有效上传结果");
            }
            verifyUploadedLocation(fileInfo, handle);
            return new FileStorageWriteResult(fileInfo.getUrl(), handle.encodeLocator());
        } catch (RuntimeException exception) {
            if (physicalWriteMayHaveStarted) {
                try {
                    deletePhysicalAndConfirm(storage, handle);
                    cleanupRecorderBestEffort(storage, handle);
                } catch (RuntimeException cleanupException) {
                    exception.addSuppressed(cleanupException);
                }
            }
            throw exception;
        }
    }

    /**
     * 按句柄固定的平台和对象键读取物理文件。
     *
     * <p>MinIO 使用已配置客户端直接读取，兼容私有桶和内网端点；其他平台仅在能够
     * 精确推导公开 URL 时使用现有读取能力。</p>
     *
     * @param handle x-file-storage 可恢复写句柄
     * @return 文件输入流，由调用方关闭
     * @throws IOException 平台不支持精确读取或对象读取失败时抛出
     */
    @Override
    public InputStream readRecoverable(FileStorageWriteHandle handle) throws IOException {
        FileStorage storage = requireStorage(handle);
        requirePersistedBasePathSupport(storage, handle);
        FileInfo fileInfo = toFileInfo(handle);
        if (storage instanceof MinioFileStorage minioStorage) {
            try {
                return minioStorage.getClient().getObject(
                        GetObjectArgs.builder()
                                .bucket(minioStorage.getBucketName())
                                .object(storage.getFileKey(fileInfo))
                                .build());
            } catch (Exception exception) {
                throw new IOException("读取 MinIO 可恢复文件失败", exception);
            }
        }
        String url = deriveUrlBestEffort(storage, fileInfo);
        if (!StringUtils.hasText(url)) {
            throw new IOException("当前 x-file-storage 平台不支持可恢复文件读取: "
                    + storage.getClass().getName());
        }
        try {
            return readStream(url);
        } catch (RuntimeException exception) {
            throw new IOException("读取 x-file-storage 可恢复文件失败", exception);
        }
    }

    /**
     * 直接调用句柄指定平台的物理删除与存在检查，绕过依赖 URL 记录的聚合删除路径。
     *
     * @param handle x-file-storage 可恢复写句柄
     * @throws RuntimeException 删除后物理对象仍存在时抛出
     */
    @Override
    public void deleteRecoverable(FileStorageWriteHandle handle) {
        FileStorage storage = requireStorage(handle);
        requirePersistedBasePathSupport(storage, handle);
        deletePhysicalAndConfirm(storage, handle);
        cleanupRecorderBestEffort(storage, handle);
    }

    /**
     * 直接检查句柄指定平台上的物理对象，不依赖 Redis 或其他 FileRecorder 记录。
     *
     * @param handle x-file-storage 可恢复写句柄
     * @return 物理对象存在时返回 true
     */
    @Override
    public boolean existsRecoverable(FileStorageWriteHandle handle) {
        FileStorage storage = requireStorage(handle);
        requirePersistedBasePathSupport(storage, handle);
        return storage.exists(toFileInfo(handle));
    }

    /**
     * 校验句柄并取得其固定平台。
     *
     * @param handle 待处理句柄
     * @return 句柄指定的具体平台存储
     */
    private FileStorage requireStorage(FileStorageWriteHandle handle) {
        if (handle == null) {
            throw new IllegalArgumentException("x-file-storage 写句柄不能为空");
        }
        if (!RECOVERABLE_BACKEND.equals(handle.getBackend()) || !StringUtils.hasText(handle.getPlatform())) {
            throw new IllegalArgumentException("文件存储写句柄不属于 x-file-storage 后端");
        }
        FileStorage storage = fileStorageService.getFileStorage(handle.getPlatform());
        if (storage == null) {
            throw new IllegalStateException("x-file-storage 平台不存在: " + handle.getPlatform());
        }
        return storage;
    }

    /**
     * 上传时要求平台当前基础路径仍与句柄一致，因为 x-file-storage 的 save 会覆盖 FileInfo.basePath。
     *
     * @param storage 具体平台存储
     * @param handle 文件存储写句柄
     */
    private void requireCurrentBasePathForWrite(FileStorage storage, FileStorageWriteHandle handle) {
        String currentBasePath = readRequiredBasePath(storage);
        if (!Objects.equals(currentBasePath, handle.getBasePath())) {
            throw new IllegalStateException("x-file-storage 平台基础路径已变化，无法写入预先确定的位置");
        }
    }

    /**
     * 基础路径发生配置漂移时，确认具体平台的物理 key 仍实际使用句柄中的持久 basePath。
     *
     * <p>大多数对象存储使用 {@link FileStorage#getFileKey(FileInfo)} 默认实现，可安全清理历史
     * basePath；忽略 FileInfo.basePath 的平台会 fail-fast，避免删除当前新目录下的同名对象。</p>
     *
     * @param storage 具体平台存储
     * @param handle 文件存储写句柄
     */
    private void requirePersistedBasePathSupport(FileStorage storage, FileStorageWriteHandle handle) {
        String currentBasePath = readRequiredBasePath(storage);
        if (Objects.equals(currentBasePath, handle.getBasePath())) {
            return;
        }
        FileInfo fileInfo = toFileInfo(handle);
        String expectedKey = handle.getBasePath() + physicalPath(handle) + handle.getFilename();
        if (!Objects.equals(expectedKey, storage.getFileKey(fileInfo))) {
            throw new IllegalStateException("x-file-storage 平台基础路径已变化，且当前平台无法按持久 basePath 定位");
        }
    }

    /**
     * 反射调用具体平台公开的 getBasePath 方法。
     *
     * @param storage 具体平台存储
     * @return 基础路径，平台返回 null 时规范为空字符串
     * @throws RuntimeException 平台未公开兼容方法或调用失败时抛出
     */
    private String readRequiredBasePath(FileStorage storage) {
        try {
            Method method = storage.getClass().getMethod("getBasePath");
            if (!String.class.equals(method.getReturnType())) {
                throw new IllegalStateException("x-file-storage 平台 getBasePath 返回类型不是 String: "
                        + storage.getClass().getName());
            }
            String basePath = (String) method.invoke(storage);
            return basePath == null ? "" : basePath;
        } catch (NoSuchMethodException exception) {
            throw new IllegalStateException("x-file-storage 平台未公开 getBasePath: "
                    + storage.getClass().getName(), exception);
        } catch (IllegalAccessException | InvocationTargetException exception) {
            throw new IllegalStateException("读取 x-file-storage 平台基础路径失败: "
                    + storage.getClass().getName(), exception);
        }
    }

    /**
     * 校验 x-file-storage 实际上传位置与预先持久化句柄完全一致。
     *
     * @param fileInfo 实际上传结果
     * @param handle 预先准备的句柄
     */
    private void verifyUploadedLocation(FileInfo fileInfo, FileStorageWriteHandle handle) {
        String actualBasePath = fileInfo.getBasePath() == null ? "" : fileInfo.getBasePath();
        String actualPath = fileInfo.getPath() == null ? "" : fileInfo.getPath();
        if (!handle.getPlatform().equals(fileInfo.getPlatform())
                || !handle.getBasePath().equals(actualBasePath)
                || !physicalPath(handle).equals(actualPath)
                || !handle.getFilename().equals(fileInfo.getFilename())) {
            throw new IllegalStateException("x-file-storage 实际上传位置与恢复句柄不一致");
        }
    }

    /**
     * 构造仅包含精确物理定位字段的 FileInfo。
     *
     * @param handle 文件存储写句柄
     * @return 供具体平台直接删除或检查的文件信息
     */
    private FileInfo toFileInfo(FileStorageWriteHandle handle) {
        return new FileInfo()
                .setPlatform(handle.getPlatform())
                .setBasePath(handle.getBasePath())
                .setPath(physicalPath(handle))
                .setFilename(handle.getFilename());
    }

    /**
     * 将句柄中的安全相对目录转换为 x-file-storage 直接拼接 basePath 所需的物理目录。
     *
     * <p>当前配置常使用不带尾斜杠的 basePath；此时必须补一个前导斜杠，避免生成
     * {@code attachmentskill-content/...} 一类错误对象键。</p>
     *
     * @param handle 文件存储写句柄
     * @return 传给 x-file-storage 的精确物理目录
     */
    private String physicalPath(FileStorageWriteHandle handle) {
        if (handle.getBasePath().isEmpty() || handle.getBasePath().endsWith("/")) {
            return handle.getPath();
        }
        return "/" + handle.getPath();
    }

    /**
     * 直接删除具体平台物理对象，并以随后 exists 结果作为成功判据。
     *
     * @param storage 具体平台存储
     * @param handle 文件存储写句柄
     */
    private void deletePhysicalAndConfirm(FileStorage storage, FileStorageWriteHandle handle) {
        FileInfo fileInfo = toFileInfo(handle);
        boolean deleted;
        try {
            deleted = storage.delete(fileInfo);
        } catch (RuntimeException exception) {
            final boolean stillExists;
            try {
                stillExists = storage.exists(fileInfo);
            } catch (RuntimeException existsException) {
                exception.addSuppressed(existsException);
                throw exception;
            }
            if (!stillExists) {
                return;
            }
            throw exception;
        }
        if (storage.exists(fileInfo)) {
            throw new IllegalStateException("x-file-storage 删除后物理对象仍存在，platform="
                    + handle.getPlatform() + ", path=" + handle.getPath() + handle.getFilename()
                    + ", deleteResult=" + deleted);
        }
    }

    /**
     * 在物理删除已经确认成功后，尽力清理可推导 URL 对应的 recorder 记录。
     *
     * <p>记录不存在、平台不能公开推导 URL 或清理失败均不改变物理删除成功结果。</p>
     *
     * @param storage 具体平台存储
     * @param handle 文件存储写句柄
     */
    private void cleanupRecorderBestEffort(FileStorage storage, FileStorageWriteHandle handle) {
        try {
            FileRecorder recorder = fileStorageService.getFileRecorder();
            if (recorder == null) {
                return;
            }
            String url = deriveUrlBestEffort(storage, toFileInfo(handle));
            if (!StringUtils.hasText(url)) {
                return;
            }
            if (!recorder.delete(url)) {
                LOG.debug("x-file-storage recorder 中没有可清理记录，url={}", url);
            }
        } catch (RuntimeException exception) {
            LOG.warn("物理文件已删除，但清理 x-file-storage recorder 记录失败，platform={}",
                    handle.getPlatform(), exception);
        }
    }

    /**
     * 使用平台公开的 getDomain 与 getFileKey 尽力推导 recorder 使用的 URL。
     *
     * @param storage 具体平台存储
     * @param fileInfo 精确物理文件信息
     * @return 可推导 URL；平台不支持时返回 null
     */
    private String deriveUrlBestEffort(FileStorage storage, FileInfo fileInfo) {
        try {
            Method method = storage.getClass().getMethod("getDomain");
            if (!String.class.equals(method.getReturnType())) {
                return null;
            }
            String domain = (String) method.invoke(storage);
            if (domain == null) {
                return null;
            }
            return domain + storage.getFileKey(fileInfo);
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | RuntimeException exception) {
            LOG.debug("当前 x-file-storage 平台无法推导 recorder URL: {}", storage.getClass().getName());
            return null;
        }
    }
}
