package com.njcn.gather.device.service;

import com.njcn.gather.device.pojo.vo.PqDevImageVO;
import com.njcn.gather.device.util.PqDevImageSupport;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Stream;

@Service
public class PqDevImageFileService {

    public static final int MAX_IMAGE_COUNT = 5;

    private static final Pattern UUID_JPG_FILE_NAME = Pattern.compile(
            "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.jpg$"
    );
    private static final Pattern DEVICE_ID_PATTERN = Pattern.compile("^[A-Za-z0-9_-]{1,64}$");
    private static final String INVALID_IMAGE_NAME = "图片文件名无效";
    private static final String INVALID_IMAGE_PATH = "图片路径无效";
    private static final String INVALID_DEVICE_ID = "设备ID无效";
    private static final String IMAGE_NOT_EXISTS = "设备图片不存在";
    private static final String IMAGE_COUNT_EXCEEDED = "设备图片最多上传5张";
    private static final String READ_IMAGE_FAILED = "读取设备图片失败";
    private static final String PREVIEW_URL = "/pqDev/image/preview?id=%s&fileName=%s";
    private static final String DOWNLOAD_URL = "/pqDev/image/download?id=%s&fileName=%s";

    private final String imageDir;

    public PqDevImageFileService(@Value("${device.image.dir:}") String imageDir) {
        this.imageDir = imageDir;
    }

    public List<PqDevImageVO> listImages(String devId) {
        Path deviceDir = deviceDir(devId);
        if (!Files.isDirectory(deviceDir)) {
            return Collections.emptyList();
        }

        List<Path> imagePaths = new ArrayList<>();
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(deviceDir)) {
            for (Path path : stream) {
                if (Files.isRegularFile(path) && isValidImageFileName(path.getFileName().toString())) {
                    imagePaths.add(path);
                }
            }
        } catch (IOException ex) {
            throw new IllegalStateException(READ_IMAGE_FAILED, ex);
        }

        Collections.sort(imagePaths, new Comparator<Path>() {
            @Override
            public int compare(Path left, Path right) {
                int modifiedCompare = Long.compare(lastModified(left), lastModified(right));
                if (modifiedCompare != 0) {
                    return modifiedCompare;
                }
                return left.getFileName().toString().compareTo(right.getFileName().toString());
            }
        });

        List<PqDevImageVO> images = new ArrayList<>();
        for (Path imagePath : imagePaths) {
            images.add(buildImageVO(devId, imagePath.getFileName().toString()));
        }
        return images;
    }

    public void validateAddCount(List<MultipartFile> newImages) {
        if (effectiveNewImageCount(newImages) > MAX_IMAGE_COUNT) {
            throw new IllegalArgumentException(IMAGE_COUNT_EXCEEDED);
        }
    }

    public void validateFinalCount(String devId, List<String> deleteImageNames, List<MultipartFile> newImages) {
        validateAddCount(newImages);
        Set<String> existingNames = currentImageNameSet(devId);
        validateExistingImageNames(devId, deleteImageNames);

        int finalCount = existingNames.size() - uniqueCount(deleteImageNames) + effectiveNewImageCount(newImages);
        if (finalCount > MAX_IMAGE_COUNT) {
            throw new IllegalArgumentException(IMAGE_COUNT_EXCEEDED);
        }
    }

    public List<PqDevImageVO> saveImages(String devId, List<MultipartFile> newImages) throws IOException {
        validateAddCount(newImages);
        if (newImages == null || newImages.isEmpty()) {
            return Collections.emptyList();
        }

        Files.createDirectories(deviceDir(devId));
        List<PqDevImageVO> savedImages = new ArrayList<>();
        for (MultipartFile image : newImages) {
            if (image == null || image.isEmpty()) {
                continue;
            }
            String fileName = UUID.randomUUID().toString().toLowerCase(Locale.ROOT) + ".jpg";
            Path path = imagePath(devId, fileName);
            writeImage(path, PqDevImageSupport.processUploadToJpeg(image));
            savedImages.add(buildImageVO(devId, fileName));
        }
        return savedImages;
    }

    public void deleteImages(String devId, List<String> deleteImageNames) throws IOException {
        validateExistingImageNames(devId, deleteImageNames);
        if (deleteImageNames == null || deleteImageNames.isEmpty()) {
            return;
        }
        Set<String> deletedNames = new HashSet<>();
        for (String fileName : deleteImageNames) {
            if (deletedNames.add(fileName)) {
                Files.delete(imagePath(devId, fileName));
            }
        }
    }

    public void validateImageNames(List<String> imageNames) {
        if (imageNames == null) {
            return;
        }
        for (String imageName : imageNames) {
            if (!isValidImageFileName(imageName)) {
                throw new IllegalArgumentException(INVALID_IMAGE_NAME);
            }
        }
    }

    public void validateExistingImageNames(String devId, List<String> imageNames) {
        validateImageNames(imageNames);
        if (imageNames == null || imageNames.isEmpty()) {
            return;
        }
        Set<String> existingNames = currentImageNameSet(devId);
        for (String imageName : imageNames) {
            if (!existingNames.contains(imageName)) {
                throw new IllegalArgumentException(IMAGE_NOT_EXISTS);
            }
        }
    }

    public Path imagePath(String devId, String fileName) {
        if (!isValidImageFileName(fileName)) {
            throw new IllegalArgumentException(INVALID_IMAGE_NAME);
        }
        Path deviceDir = deviceDir(devId);
        Path path = deviceDir.resolve(fileName).normalize();
        if (!path.startsWith(deviceDir)) {
            throw new IllegalArgumentException(INVALID_IMAGE_PATH);
        }
        return path;
    }

    public long imageSize(String devId, String fileName) throws IOException {
        Path path = imagePath(devId, fileName);
        if (!Files.isRegularFile(path)) {
            throw new IllegalArgumentException(IMAGE_NOT_EXISTS);
        }
        return Files.size(path);
    }

    public void writeImage(String devId, String fileName, OutputStream outputStream) throws IOException {
        Path path = imagePath(devId, fileName);
        if (!Files.isRegularFile(path)) {
            throw new IllegalArgumentException(IMAGE_NOT_EXISTS);
        }
        Files.copy(path, outputStream);
    }

    public void deleteDeviceDirectoryQuietly(String devId) {
        Path deviceDir = deviceDir(devId);
        if (!Files.exists(deviceDir)) {
            return;
        }
        try (Stream<Path> stream = Files.walk(deviceDir)) {
            List<Path> paths = new ArrayList<>();
            Iterator<Path> iterator = stream.iterator();
            while (iterator.hasNext()) {
                paths.add(iterator.next());
            }
            Collections.sort(paths, new Comparator<Path>() {
                @Override
                public int compare(Path left, Path right) {
                    return right.compareTo(left);
                }
            });
            for (Path path : paths) {
                Files.deleteIfExists(path);
            }
        } catch (IOException ignored) {
        }
    }

    private void writeImage(Path imagePath, byte[] imageBytes) throws IOException {
        Files.createDirectories(imagePath.getParent());
        Files.write(imagePath, imageBytes, StandardOpenOption.CREATE_NEW);
    }

    private Path rootDir() {
        Path root;
        if (StringUtils.hasText(imageDir)) {
            root = Paths.get(imageDir);
        } else {
            root = Paths.get(System.getProperty("user.dir"), "data", "device-images");
        }
        return root.toAbsolutePath().normalize();
    }

    private Path deviceDir(String devId) {
        if (!StringUtils.hasText(devId) || !DEVICE_ID_PATTERN.matcher(devId).matches()) {
            throw new IllegalArgumentException(INVALID_DEVICE_ID);
        }
        Path rootDir = rootDir();
        Path deviceDir = rootDir.resolve(devId).normalize();
        if (!deviceDir.startsWith(rootDir)) {
            throw new IllegalArgumentException(INVALID_IMAGE_PATH);
        }
        return deviceDir;
    }

    private PqDevImageVO buildImageVO(String devId, String fileName) {
        return new PqDevImageVO(
                fileName,
                String.format(PREVIEW_URL, devId, fileName),
                String.format(DOWNLOAD_URL, devId, fileName)
        );
    }

    private Set<String> currentImageNameSet(String devId) {
        Set<String> names = new HashSet<>();
        for (PqDevImageVO image : listImages(devId)) {
            names.add(image.getFileName());
        }
        return names;
    }

    private int effectiveNewImageCount(List<MultipartFile> newImages) {
        if (newImages == null || newImages.isEmpty()) {
            return 0;
        }
        int count = 0;
        for (MultipartFile image : newImages) {
            if (image != null && !image.isEmpty()) {
                count++;
            }
        }
        return count;
    }

    private int uniqueCount(List<String> names) {
        if (names == null || names.isEmpty()) {
            return 0;
        }
        return new HashSet<>(names).size();
    }

    private boolean isValidImageFileName(String fileName) {
        return fileName != null && UUID_JPG_FILE_NAME.matcher(fileName).matches();
    }

    private long lastModified(Path path) {
        try {
            return Files.getLastModifiedTime(path).toMillis();
        } catch (IOException ex) {
            return 0L;
        }
    }
}
