package com.njcn.gather.system.resource.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.common.pojo.enums.common.DataStateEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.system.config.PathConfig;
import com.njcn.gather.system.resource.mapper.ResourceManageMapper;
import com.njcn.gather.system.resource.pojo.enums.ResourceManageResponseEnum;
import com.njcn.gather.system.resource.pojo.param.ResourceManageParam;
import com.njcn.gather.system.resource.pojo.po.ResourceManage;
import com.njcn.gather.system.resource.pojo.vo.PlayVO;
import com.njcn.gather.system.resource.pojo.vo.ResourceManageVO;
import com.njcn.gather.system.resource.service.IResourceManageService;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.RequestUtil;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;


@Slf4j
@Service
@RequiredArgsConstructor
public class ResourceManageServiceImpl extends ServiceImpl<ResourceManageMapper, ResourceManage> implements IResourceManageService {

    private static final long MAX_FILE_SIZE = 250L * 1024L * 1024L;
    private static final long PLAY_TOKEN_TTL_MILLIS = 30L * 60L * 1000L;
    private static final String MP4_SUFFIX = ".mp4";
    private static final String RELATIVE_VIDEO_ROOT = "resources/videos";
    private static final int BUFFER_SIZE = 8192;

    private static final ConcurrentHashMap<String, PlayToken> PLAY_TOKEN_CACHE = new ConcurrentHashMap<>();

    //    @Value("${resource.videoDir:D:/data/resources/videos}")
//    private String videoDir;
    private final PathConfig pathConfig;

    @Override
    public Page<ResourceManageVO> list(ResourceManageParam.QueryParam queryParam) {
        QueryWrapper<ResourceManage> wrapper = new QueryWrapper<>();
        wrapper.like(StrUtil.isNotBlank(queryParam.getName()), "Name", queryParam.getName())
                .like(StrUtil.isNotBlank(queryParam.getFileName()), "File_Name", queryParam.getFileName())
                .eq("State", DataStateEnum.ENABLE.getCode())
                .orderByDesc("Create_Time");

        Page<ResourceManage> page = this.page(
                new Page<>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam)),
                wrapper
        );

        List<ResourceManageVO> records = page.getRecords().stream().map(resource -> {
            ResourceManageVO vo = new ResourceManageVO();
            BeanUtil.copyProperties(resource, vo);
            return vo;
        }).collect(Collectors.toList());

        Page<ResourceManageVO> result = new Page<>(PageFactory.getPageNum(queryParam), PageFactory.getPageSize(queryParam));
        result.setTotal(page.getTotal());
        result.setPages(page.getPages());
        result.setCurrent(page.getCurrent());
        result.setSize(page.getSize());
        result.setRecords(records);
        return result;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean add(ResourceManageParam resourceManageParam) {
        validateAddParam(resourceManageParam);

        MultipartFile file = resourceManageParam.getFile();
        String originalFilename = file.getOriginalFilename();
        String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String savedFileName = UUID.randomUUID().toString().replace("-", "") + MP4_SUFFIX;
        Path rootPath = Paths.get(pathConfig.getResourcePath()).toAbsolutePath().normalize();
        Path targetDir = rootPath.resolve(dateDir).normalize();
        Path targetFile = targetDir.resolve(savedFileName).normalize();
        ensurePathInRoot(rootPath, targetFile);

        String relativePath = RELATIVE_VIDEO_ROOT + "/" + dateDir + "/" + savedFileName;

        try {
            Files.createDirectories(targetDir);
            long usableSpace = targetDir.toFile().getUsableSpace();
            if (usableSpace > 0 && usableSpace < file.getSize()) {
                throw new BusinessException(ResourceManageResponseEnum.DISK_SPACE_NOT_ENOUGH);
            }
            file.transferTo(targetFile.toFile());
        } catch (BusinessException e) {
            throw e;
        } catch (IOException e) {
            throw new BusinessException(ResourceManageResponseEnum.FILE_UPLOAD_FAILED);
        }

        ResourceManage resourceManage = new ResourceManage();
        resourceManage.setId(UUID.randomUUID().toString().replace("-", ""));
        resourceManage.setName(resourceManageParam.getName().trim());
        resourceManage.setRemark(resourceManageParam.getRemark().trim());
        resourceManage.setFileName(originalFilename);
        resourceManage.setFileSize(file.getSize());
        resourceManage.setRelativePath(relativePath);
        resourceManage.setState(DataStateEnum.ENABLE.getCode());

        try {
            boolean saved = this.save(resourceManage);
            if (!saved) {
                deleteQuietly(targetFile);
            }
            return saved;
        } catch (RuntimeException e) {
            deleteQuietly(targetFile);
            throw e;
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean update(ResourceManageParam.UpdateParam updateParam) {
        validateUpdateParam(updateParam);
        getEnabledResource(updateParam.getId());
        return this.lambdaUpdate()
                .set(ResourceManage::getName, updateParam.getName().trim())
                .set(ResourceManage::getRemark, updateParam.getRemark().trim())
                .eq(ResourceManage::getId, updateParam.getId())
                .eq(ResourceManage::getState, DataStateEnum.ENABLE.getCode())
                .update();
    }

    @Override
    public PlayVO play(String id) {
        ResourceManage resourceManage = getEnabledResource(id);
        Path filePath = resolveResourcePath(resourceManage);
        if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
            throw new BusinessException(ResourceManageResponseEnum.RESOURCE_FILE_NOT_EXIST);
        }

        String token = UUID.randomUUID().toString().replace("-", "");
        PlayToken playToken = new PlayToken();
        playToken.setResourceId(id);
        playToken.setUserId(RequestUtil.getUserId());
        playToken.setExpireTime(System.currentTimeMillis() + PLAY_TOKEN_TTL_MILLIS);
        PLAY_TOKEN_CACHE.put(token, playToken);
        clearExpiredTokens();

        return new PlayVO("/resourceManage/stream?id=" + id + "&token=" + token);
    }

    @Override
    public void stream(String id, String token, String rangeHeader, HttpServletResponse response) {
        if (!validatePlayToken(id, token)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return;
        }

        ResourceManage resourceManage = getEnabledResource(id);
        Path filePath = resolveResourcePath(resourceManage);
        if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        try {
            long fileLength = Files.size(filePath);
            Range range = parseRange(rangeHeader, fileLength);
            String encodedFileName = URLEncoder.encode(resourceManage.getFileName(), "UTF-8")
                    .replaceAll("\\+", "%20");

            response.setContentType("video/mp4");
            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("Content-Disposition", "inline; filename*=UTF-8''" + encodedFileName);
            response.setHeader("Content-Length", String.valueOf(range.getLength()));
            if (range.isPartial()) {
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
                response.setHeader("Content-Range", "bytes " + range.getStart() + "-" + range.getEnd() + "/" + fileLength);
            } else {
                response.setStatus(HttpServletResponse.SC_OK);
            }

            writeRange(filePath, range, response);
        } catch (IllegalArgumentException e) {
            response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
        } catch (IOException e) {
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    private void validateAddParam(ResourceManageParam param) {
        if (param == null || StrUtil.isBlank(param.getName())) {
            throw new BusinessException(ResourceManageResponseEnum.NAME_NOT_BLANK);
        }
        if (StrUtil.isBlank(param.getRemark())) {
            throw new BusinessException(ResourceManageResponseEnum.REMARK_NOT_BLANK);
        }
        MultipartFile file = param.getFile();
        if (file == null || file.isEmpty()) {
            throw new BusinessException(ResourceManageResponseEnum.FILE_NOT_NULL);
        }
        String originalFilename = file.getOriginalFilename();
        if (StrUtil.isBlank(originalFilename) || !originalFilename.toLowerCase().endsWith(MP4_SUFFIX)) {
            throw new BusinessException(ResourceManageResponseEnum.FILE_SUFFIX_ERROR);
        }
        String contentType = file.getContentType();
        if (StrUtil.isNotBlank(contentType) && !"video/mp4".equalsIgnoreCase(contentType)
                && !"application/octet-stream".equalsIgnoreCase(contentType)) {
            throw new BusinessException(ResourceManageResponseEnum.FILE_SUFFIX_ERROR);
        }
        if (file.getSize() > MAX_FILE_SIZE) {
            throw new BusinessException(ResourceManageResponseEnum.FILE_SIZE_ERROR);
        }
    }

    private void validateUpdateParam(ResourceManageParam.UpdateParam param) {
        if (param == null || StrUtil.isBlank(param.getId())) {
            throw new BusinessException(ResourceManageResponseEnum.ID_NOT_BLANK);
        }
        if (StrUtil.isBlank(param.getName())) {
            throw new BusinessException(ResourceManageResponseEnum.NAME_NOT_BLANK);
        }
        if (StrUtil.isBlank(param.getRemark())) {
            throw new BusinessException(ResourceManageResponseEnum.REMARK_NOT_BLANK);
        }
    }

    private ResourceManage getEnabledResource(String id) {
        ResourceManage resourceManage = this.lambdaQuery()
                .eq(ResourceManage::getId, id)
                .eq(ResourceManage::getState, DataStateEnum.ENABLE.getCode())
                .one();
        if (resourceManage == null) {
            throw new BusinessException(ResourceManageResponseEnum.RESOURCE_NOT_EXIST);
        }
        return resourceManage;
    }

    private Path resolveResourcePath(ResourceManage resourceManage) {
        Path rootPath = Paths.get(pathConfig.getResourcePath()).toAbsolutePath().normalize();
        String relativePath = resourceManage.getRelativePath().replace("\\", "/");
        String prefix = RELATIVE_VIDEO_ROOT + "/";
        if (!relativePath.startsWith(prefix)) {
            throw new BusinessException(ResourceManageResponseEnum.RESOURCE_FILE_NOT_EXIST);
        }
        String relativeToVideoRoot = relativePath.substring(prefix.length());
        Path filePath = rootPath.resolve(relativeToVideoRoot).normalize();
        ensurePathInRoot(rootPath, filePath);
        return filePath;
    }

    private void ensurePathInRoot(Path rootPath, Path targetPath) {
        if (!targetPath.startsWith(rootPath)) {
            throw new BusinessException(ResourceManageResponseEnum.RESOURCE_FILE_NOT_EXIST);
        }
    }

    private boolean validatePlayToken(String id, String token) {
        if (StrUtil.isBlank(id) || StrUtil.isBlank(token)) {
            return false;
        }
        PlayToken playToken = PLAY_TOKEN_CACHE.get(token);
        if (playToken == null || !id.equals(playToken.getResourceId())) {
            return false;
        }
        if (playToken.getExpireTime() < System.currentTimeMillis()) {
            PLAY_TOKEN_CACHE.remove(token);
            return false;
        }
        return true;
    }

    private void clearExpiredTokens() {
        long now = System.currentTimeMillis();
        PLAY_TOKEN_CACHE.entrySet().removeIf(entry -> entry.getValue().getExpireTime() < now);
    }

    private Range parseRange(String rangeHeader, long fileLength) {
        if (StrUtil.isBlank(rangeHeader)) {
            return new Range(0, fileLength - 1, false);
        }
        if (!rangeHeader.startsWith("bytes=")) {
            throw new IllegalArgumentException("Invalid range");
        }
        String rangeValue = rangeHeader.substring("bytes=".length());
        String[] parts = rangeValue.split("-", -1);
        if (parts.length != 2) {
            throw new IllegalArgumentException("Invalid range");
        }

        long start;
        long end;
        if (StrUtil.isBlank(parts[0])) {
            long suffixLength = Long.parseLong(parts[1]);
            if (suffixLength <= 0) {
                throw new IllegalArgumentException("Invalid range");
            }
            start = Math.max(fileLength - suffixLength, 0);
            end = fileLength - 1;
        } else {
            start = Long.parseLong(parts[0]);
            end = StrUtil.isBlank(parts[1]) ? fileLength - 1 : Long.parseLong(parts[1]);
        }

        if (start < 0 || end < start || start >= fileLength) {
            throw new IllegalArgumentException("Invalid range");
        }
        end = Math.min(end, fileLength - 1);
        return new Range(start, end, true);
    }

    private void writeRange(Path filePath, Range range, HttpServletResponse response) throws IOException {
        try (InputStream inputStream = Files.newInputStream(filePath);
             ServletOutputStream outputStream = response.getOutputStream()) {
            long skipped = inputStream.skip(range.getStart());
            while (skipped < range.getStart()) {
                long current = inputStream.skip(range.getStart() - skipped);
                if (current <= 0) {
                    break;
                }
                skipped += current;
            }

            byte[] buffer = new byte[BUFFER_SIZE];
            long bytesRemaining = range.getLength();
            while (bytesRemaining > 0) {
                int readLength = (int) Math.min(buffer.length, bytesRemaining);
                int read = inputStream.read(buffer, 0, readLength);
                if (read == -1) {
                    break;
                }
                outputStream.write(buffer, 0, read);
                bytesRemaining -= read;
            }
            outputStream.flush();
        }
    }

    private void deleteQuietly(Path path) {
        try {
            Files.deleteIfExists(path);
        } catch (IOException e) {
            log.warn("删除资源文件失败: {}", path, e);
        }
    }

    @Data
    private static class PlayToken {
        private String resourceId;
        private String userId;
        private long expireTime;
    }

    @Data
    private static class Range {
        private final long start;
        private final long end;
        private final boolean partial;

        private long getLength() {
            return end - start + 1;
        }
    }
}



