package com.njcn.rdms.module.system.service.file;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.google.common.annotations.VisibleForTesting;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.http.HttpUtils;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.security.core.service.SecurityFrameworkService;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.system.controller.admin.file.vo.file.FileCreateReqVO;
import com.njcn.rdms.module.system.controller.admin.file.vo.file.FilePageReqVO;
import com.njcn.rdms.module.system.controller.admin.file.vo.file.FilePresignedUrlRespVO;
import com.njcn.rdms.module.system.dal.dataobject.file.FileContentDO;
import com.njcn.rdms.module.system.dal.dataobject.file.FileDO;
import com.njcn.rdms.module.system.dal.mysql.file.FileContentMapper;
import com.njcn.rdms.module.system.dal.mysql.file.FileMapper;
import com.njcn.rdms.module.system.framework.file.core.client.FileClient;
import com.njcn.rdms.module.system.framework.file.core.utils.FileTypeUtils;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;

import java.util.List;

import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.FILE_DELETE_NO_PERMISSION;
import static com.njcn.rdms.module.system.enums.ErrorCodeConstants.FILE_NOT_EXISTS;

/**
 * 文件 Service 实现类
 *
 * @author hongawen
 */
@Service
public class FileServiceImpl implements FileService {

    /**
     * 上传文件的前缀，是否包含日期（yyyyMMdd）
     * <p>
     * 目的：按照日期，进行分目录
     */
    static boolean PATH_PREFIX_DATE_ENABLE = true;
    @Resource
    private FileConfigService fileConfigService;

    @Resource
    private FileMapper fileMapper;

    @Resource
    private FileContentMapper fileContentMapper;

    @Resource
    private SecurityFrameworkService securityFrameworkService;

    @Override
    public PageResult<FileDO> getFilePage(FilePageReqVO pageReqVO) {
        return fileMapper.selectPage(pageReqVO);
    }

    @Override
    @SneakyThrows
    public FileDO createFile(byte[] content, String name, String directory, String type) {
        // 1.1 处理 type 为空的情况
        if (StrUtil.isEmpty(type)) {
            type = FileTypeUtils.getMineType(content, name);
        }
        // 1.2 处理 name 为空的情况
        if (StrUtil.isEmpty(name)) {
            name = DigestUtil.sha256Hex(content);
        }
        if (StrUtil.isEmpty(FileUtil.extName(name))) {
            // 如果 name 没有后缀 type，则补充后缀
            String extension = FileTypeUtils.getExtension(type);
            if (StrUtil.isNotEmpty(extension)) {
                name = name + extension;
            }
        }

        // 2.1 生成上传的 path，需要保证唯一
        String path = generateUploadPath(name, directory);
        // 2.2 上传到文件存储器
        FileClient client = fileConfigService.getMasterFileClient();
        Assert.notNull(client, "客户端(master) 不能为空");
        String url = client.upload(content, path, type);

        // 3. 保存到数据库
        FileDO file = new FileDO().setConfigId(client.getId())
                .setName(name).setPath(path).setUrl(url)
                .setType(type).setSize((long) content.length);
        fileMapper.insert(file);
        FileContentDO contentDO = new FileContentDO().setConfigId(file.getId())
                .setPath(path).setContent(content);
        fileContentMapper.insert(contentDO);
        return file;
    }

    @VisibleForTesting
    String generateUploadPath(String name, String directory) {
        // 1. 生成前缀，并将存储文件名切换为安全 key：
        // path 仅承担对象存储 key / 路径职责，不再复用原文件名，避免 %、&、中文等字符进入 URI 路径。
        String prefix = null;
        if (PATH_PREFIX_DATE_ENABLE) {
            prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN);
        }
        String ext = FileUtil.extName(name);
        String storageName = IdUtil.fastSimpleUUID();
        if (StrUtil.isNotEmpty(ext)) {
            storageName = storageName + StrUtil.DOT + ext;
        }

        // 2. 先拼接 prefix 前缀
        if (StrUtil.isNotEmpty(prefix)) {
            storageName = prefix + StrUtil.SLASH + storageName;
        }
        // 3. 最后拼接 directory 目录
        if (StrUtil.isNotEmpty(directory)) {
            storageName = directory + StrUtil.SLASH + storageName;
        }
        return storageName;
    }

    @Override
    @SneakyThrows
    public FilePresignedUrlRespVO presignPutUrl(String name, String directory) {
        // 1. 生成上传的 path，需要保证唯一
        String path = generateUploadPath(name, directory);

        // 2. 获取文件预签名地址
        FileClient fileClient = fileConfigService.getMasterFileClient();
        String uploadUrl = fileClient.presignPutUrl(path);
        String visitUrl = fileClient.presignGetUrl(path, null);
        return new FilePresignedUrlRespVO().setConfigId(fileClient.getId())
                .setPath(path).setUploadUrl(uploadUrl).setUrl(visitUrl);
    }

    @Override
    public String presignGetUrl(String url, Integer expirationSeconds) {
        FileClient fileClient = fileConfigService.getMasterFileClient();
        return fileClient.presignGetUrl(url, expirationSeconds);
    }

    @Override
    public Long createFile(FileCreateReqVO createReqVO) {
        createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的：移除私有桶情况下，URL 的签名参数
        FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
        fileMapper.insert(file);
        return file.getId();
    }

    @Override
    public FileDO getFile(Long id) {
        return validateFileExists(id);
    }

    @Override
    public FileDO getFileByConfigIdAndPath(Long configId, String path) {
        if (configId == null || StrUtil.isBlank(path)) {
            return null;
        }
        return fileMapper.selectByConfigIdAndPath(configId, path);
    }

    @Override
    public FileDO getFileByUrl(String url) {
        if (StrUtil.isBlank(url)) {
            return null;
        }
        return fileMapper.selectByUrl(url);
    }

    @Override
    public void deleteFile(Long id) throws Exception {
        // 校验存在
        FileDO file = validateFileExists(id);
        // 校验删除权限：上传者本人，或具备文件删除权限者
        validateFileDeletePermission(file);

        // 从文件存储器中删除
        FileClient client = fileConfigService.getFileClient(file.getConfigId());
        Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());
        client.delete(file.getPath());

        // 删除记录
        fileMapper.deleteById(id);
    }

    @Override
    @SneakyThrows
    public void deleteFileList(List<Long> ids) {
        // 删除文件
        List<FileDO> files = fileMapper.selectByIds(ids);
        // 逐个校验删除权限，任一越权即整体拒绝
        files.forEach(this::validateFileDeletePermission);
        for (FileDO file : files) {
            // 获取客户端
            FileClient client = fileConfigService.getFileClient(file.getConfigId());
            Assert.notNull(client, "客户端({}) 不能为空", file.getPath());
            // 删除文件
            client.delete(file.getPath());
        }

        // 删除记录
        fileMapper.deleteByIds(ids);
    }

    private FileDO validateFileExists(Long id) {
        FileDO fileDO = fileMapper.selectById(id);
        if (fileDO == null) {
            throw exception(FILE_NOT_EXISTS);
        }
        return fileDO;
    }

    /**
     * 校验文件删除权限。
     * <p>
     * 口径：上传者本人可删除自己上传的文件；其余情况需具备文件管理删除权限 {@code system:file:delete}（运维 / 管理员）。
     * 该接口按 id 删除、不校验业务归属，故不能对所有登录用户放开，只对"本人上传"放行。
     */
    private void validateFileDeletePermission(FileDO file) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        // 上传者本人放行（creator 存的是上传时的登录用户 id）
        if (loginUserId != null && String.valueOf(loginUserId).equals(file.getCreator())) {
            return;
        }
        // 兜底：具备文件删除权限者（运维 / 管理员）可删除任意文件
        if (securityFrameworkService.hasPermission("system:file:delete")) {
            return;
        }
        throw exception(FILE_DELETE_NO_PERMISSION);
    }

    @Override
    public byte[] getFileContent(Long configId, String path) throws Exception {
        // 软删 / 不存在的记录直接拒绝
        // 该方法被匿名图片代理 (FileController#getFileContent) 复用，必须先按 DB 校验存在性，
        // 否则历史路径在 infra_file 被逻辑删除后，外部仍能匿名拉到对象存储字节
        if (fileMapper.selectByConfigIdAndPath(configId, path) == null) {
            return null;
        }
        FileClient client = fileConfigService.getFileClient(configId);
        Assert.notNull(client, "客户端({}) 不能为空", configId);
        return client.getContent(path);
    }

}
