package incheon.ags.pss.edit.util;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Set;

import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

/**
 * 업로드 파일의 기본 유효성(파일명/확장자 등)을 검증합니다.
 */
public final class UploadFileValidator {

    private UploadFileValidator() {
    }

    /**
     * @return 검증된 확장자(소문자)
     */
    public static String requireAllowedExtension(MultipartFile file, Set<String> allowedExtensions) {
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("업로드된 파일이 없습니다.");
        }

        String originalFilename = file.getOriginalFilename();
        if (!StringUtils.hasText(originalFilename)) {
            throw new IllegalArgumentException("파일명이 올바르지 않습니다.");
        }

        String ext = StringUtils.getFilenameExtension(originalFilename);
        if (!StringUtils.hasText(ext)) {
            throw new IllegalArgumentException("파일 확장자가 없습니다.");
        }

        String normalized = ext.toLowerCase(Locale.ROOT).trim();
        if (!allowedExtensions.contains(normalized)) {
            throw new IllegalArgumentException("허용되지 않는 파일 확장자입니다. (허용: " + String.join(", ", allowedExtensions) + ")");
        }

        return normalized;
    }

    /**
     * 확장자 기반으로 파일 포맷(실제 콘텐츠)이 정상인지 최소 수준으로 검증합니다.
     * <p>
     * NOTE: 확장자 검증을 통과한 파일이라도 내용이 다른 포맷일 수 있으므로,
     * 저장 전에 반드시 호출하는 것을 권장합니다.
     */
    public static void requireValidContent(MultipartFile file, String extensionLowercase) {
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("업로드된 파일이 없습니다.");
        }
        if (!StringUtils.hasText(extensionLowercase)) {
            throw new IllegalArgumentException("파일 확장자를 확인할 수 없습니다.");
        }

        String ext = extensionLowercase.toLowerCase(Locale.ROOT).trim();
        long size = file.getSize();
        if (size <= 0) {
            throw new IllegalArgumentException("파일이 비어 있습니다.");
        }

        try {
            switch (ext) {
                case "png":
                    requirePng(file);
                    return;
                case "jpg":
                case "jpeg":
                    requireJpeg(file);
                    return;
                case "zip":
                    requireZip(file);
                    return;
                case "glb":
                    requireGlb(file);
                    return;
                case "ifc":
                    requireIfc(file);
                    return;
                default:
                    // 허용 확장자 외에는 여기로 오지 않도록 상위에서 제한해야 합니다.
                    throw new IllegalArgumentException("지원하지 않는 파일 형식입니다.");
            }
        } catch (IOException e) {
            throw new IllegalArgumentException("파일 내용을 확인할 수 없습니다.");
        }
    }

    /**
     * 서버 로컬 디스크에 존재하는 파일을 대상으로, 확장자 기반 최소 포맷 검증을 수행합니다.
     * <p>
     * 업로드 없이 `flpth`/`trgtUrl` 등으로 파일 경로만 받아 DB에 저장하는 경우(=우회 경로)에도
     * 동일한 시그니처 검증을 적용하기 위한 용도입니다.
     */
    public static void requireValidLocalFile(Path filePath, String extensionLowercase) {
        if (filePath == null) {
            throw new IllegalArgumentException("파일 경로가 올바르지 않습니다.");
        }
        if (!StringUtils.hasText(extensionLowercase)) {
            throw new IllegalArgumentException("파일 확장자를 확인할 수 없습니다.");
        }

        String ext = extensionLowercase.toLowerCase(Locale.ROOT).trim();

        try {
            if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
                throw new IllegalArgumentException("파일을 찾을 수 없습니다.");
            }
            long size = Files.size(filePath);
            if (size <= 0) {
                throw new IllegalArgumentException("파일이 비어 있습니다.");
            }

            switch (ext) {
                case "png":
                    requirePng(filePath);
                    return;
                case "jpg":
                case "jpeg":
                    requireJpeg(filePath);
                    return;
                case "zip":
                    requireZip(filePath);
                    return;
                case "glb":
                    requireGlb(filePath);
                    return;
                case "ifc":
                    requireIfc(filePath);
                    return;
                default:
                    throw new IllegalArgumentException("지원하지 않는 파일 형식입니다.");
            }
        } catch (IOException e) {
            throw new IllegalArgumentException("파일 내용을 확인할 수 없습니다.");
        }
    }

    private static void requirePng(MultipartFile file) throws IOException {
        byte[] head = readHead(file, 8);
        // PNG signature: 89 50 4E 47 0D 0A 1A 0A
        byte[] sig = new byte[] {(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
        if (!startsWith(head, sig)) {
            throw new IllegalArgumentException("PNG 파일이 아닙니다.");
        }
    }

    private static void requirePng(Path filePath) throws IOException {
        byte[] head = readHead(filePath, 8);
        byte[] sig = new byte[] {(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
        if (!startsWith(head, sig)) {
            throw new IllegalArgumentException("PNG 파일이 아닙니다.");
        }
    }

    private static void requireJpeg(MultipartFile file) throws IOException {
        if (file.getSize() < 4) {
            throw new IllegalArgumentException("JPEG 파일이 아닙니다.");
        }

        byte[] head = readHead(file, 2);
        // JPEG SOI: FF D8
        if (!(head.length >= 2 && (head[0] == (byte) 0xFF) && (head[1] == (byte) 0xD8))) {
            throw new IllegalArgumentException("JPEG 파일이 아닙니다.");
        }

        byte[] tail = readTail(file, 2);
        // JPEG EOI: FF D9
        if (!(tail.length >= 2 && (tail[0] == (byte) 0xFF) && (tail[1] == (byte) 0xD9))) {
            throw new IllegalArgumentException("JPEG 파일이 손상되었거나 올바르지 않습니다.");
        }
    }

    private static void requireJpeg(Path filePath) throws IOException {
        long size = Files.size(filePath);
        if (size < 4) {
            throw new IllegalArgumentException("JPEG 파일이 아닙니다.");
        }

        byte[] head = readHead(filePath, 2);
        if (!(head.length >= 2 && (head[0] == (byte) 0xFF) && (head[1] == (byte) 0xD8))) {
            throw new IllegalArgumentException("JPEG 파일이 아닙니다.");
        }

        byte[] tail = readTail(filePath, 2);
        if (!(tail.length >= 2 && (tail[0] == (byte) 0xFF) && (tail[1] == (byte) 0xD9))) {
            throw new IllegalArgumentException("JPEG 파일이 손상되었거나 올바르지 않습니다.");
        }
    }

    private static void requireZip(MultipartFile file) throws IOException {
        byte[] head = readHead(file, 4);
        // ZIP local file header: PK 03 04 (가장 일반적)
        if (!(head.length >= 4 && head[0] == 'P' && head[1] == 'K' && head[2] == 0x03 && head[3] == 0x04)) {
            throw new IllegalArgumentException("ZIP 파일이 아닙니다.");
        }
    }

    private static void requireZip(Path filePath) throws IOException {
        byte[] head = readHead(filePath, 4);
        if (!(head.length >= 4 && head[0] == 'P' && head[1] == 'K' && head[2] == 0x03 && head[3] == 0x04)) {
            throw new IllegalArgumentException("ZIP 파일이 아닙니다.");
        }
    }

    private static void requireGlb(MultipartFile file) throws IOException {
        if (file.getSize() < 12) {
            throw new IllegalArgumentException("GLB 파일이 아닙니다.");
        }

        byte[] head = readHead(file, 12);
        // GLB header: magic(4)='glTF', version(4), length(4) little-endian
        if (!(head.length >= 12 && head[0] == 'g' && head[1] == 'l' && head[2] == 'T' && head[3] == 'F')) {
            throw new IllegalArgumentException("GLB 파일이 아닙니다.");
        }

        ByteBuffer buf = ByteBuffer.wrap(head).order(ByteOrder.LITTLE_ENDIAN);
        buf.position(4);
        int version = buf.getInt();
        long length = Integer.toUnsignedLong(buf.getInt());

        if (version <= 0 || version > 3) {
            throw new IllegalArgumentException("지원하지 않는 GLB 버전입니다.");
        }

        long actualSize = file.getSize();
        if (length < 12 || length != actualSize) {
            throw new IllegalArgumentException("GLB 파일 길이 정보가 올바르지 않습니다.");
        }
    }

    private static void requireGlb(Path filePath) throws IOException {
        long size = Files.size(filePath);
        if (size < 12) {
            throw new IllegalArgumentException("GLB 파일이 아닙니다.");
        }

        byte[] head = readHead(filePath, 12);
        if (!(head.length >= 12 && head[0] == 'g' && head[1] == 'l' && head[2] == 'T' && head[3] == 'F')) {
            throw new IllegalArgumentException("GLB 파일이 아닙니다.");
        }

        ByteBuffer buf = ByteBuffer.wrap(head).order(ByteOrder.LITTLE_ENDIAN);
        buf.position(4);
        int version = buf.getInt();
        long length = Integer.toUnsignedLong(buf.getInt());

        if (version <= 0 || version > 3) {
            throw new IllegalArgumentException("지원하지 않는 GLB 버전입니다.");
        }

        if (length < 12 || length != size) {
            throw new IllegalArgumentException("GLB 파일 길이 정보가 올바르지 않습니다.");
        }
    }

    private static void requireIfc(MultipartFile file) throws IOException {
        // IFC는 STEP 텍스트 기반: 파일 시작이 ISO-10303-21 로 시작하는지 확인
        byte[] head = readHead(file, 64);
        String s = new String(head, StandardCharsets.US_ASCII);
        String trimmed = ltrim(s);
        if (!trimmed.startsWith("ISO-10303-21")) {
            throw new IllegalArgumentException("IFC(STEP) 파일이 아닙니다.");
        }
    }

    private static void requireIfc(Path filePath) throws IOException {
        byte[] head = readHead(filePath, 64);
        String s = new String(head, StandardCharsets.US_ASCII);
        String trimmed = ltrim(s);
        if (!trimmed.startsWith("ISO-10303-21")) {
            throw new IllegalArgumentException("IFC(STEP) 파일이 아닙니다.");
        }
    }

    private static byte[] readHead(MultipartFile file, int n) throws IOException {
        try (InputStream is = file.getInputStream()) {
            byte[] buf = new byte[n];
            int read = 0;
            while (read < n) {
                int r = is.read(buf, read, n - read);
                if (r < 0) break;
                read += r;
            }
            if (read == n) return buf;

            byte[] resized = new byte[Math.max(read, 0)];
            System.arraycopy(buf, 0, resized, 0, resized.length);
            return resized;
        }
    }

    private static byte[] readHead(Path filePath, int n) throws IOException {
        try (InputStream is = Files.newInputStream(filePath)) {
            byte[] buf = new byte[n];
            int read = 0;
            while (read < n) {
                int r = is.read(buf, read, n - read);
                if (r < 0) break;
                read += r;
            }
            if (read == n) return buf;

            byte[] resized = new byte[Math.max(read, 0)];
            System.arraycopy(buf, 0, resized, 0, resized.length);
            return resized;
        }
    }

    private static byte[] readTail(MultipartFile file, int n) throws IOException {
        long size = file.getSize();
        if (size <= 0) return new byte[0];

        long skip = Math.max(0, size - n);
        try (InputStream is = file.getInputStream()) {
            long skippedTotal = 0;
            while (skippedTotal < skip) {
                long s = is.skip(skip - skippedTotal);
                if (s <= 0) break;
                skippedTotal += s;
            }

            byte[] buf = new byte[n];
            int read = 0;
            while (read < n) {
                int r = is.read(buf, read, n - read);
                if (r < 0) break;
                read += r;
            }
            if (read == n) return buf;

            byte[] resized = new byte[Math.max(read, 0)];
            System.arraycopy(buf, 0, resized, 0, resized.length);
            return resized;
        }
    }

    private static byte[] readTail(Path filePath, int n) throws IOException {
        long size = Files.size(filePath);
        if (size <= 0) return new byte[0];

        long skip = Math.max(0, size - n);
        try (InputStream is = Files.newInputStream(filePath)) {
            long skippedTotal = 0;
            while (skippedTotal < skip) {
                long s = is.skip(skip - skippedTotal);
                if (s <= 0) break;
                skippedTotal += s;
            }

            byte[] buf = new byte[n];
            int read = 0;
            while (read < n) {
                int r = is.read(buf, read, n - read);
                if (r < 0) break;
                read += r;
            }
            if (read == n) return buf;

            byte[] resized = new byte[Math.max(read, 0)];
            System.arraycopy(buf, 0, resized, 0, resized.length);
            return resized;
        }
    }

    private static boolean startsWith(byte[] actual, byte[] expectedPrefix) {
        if (actual == null || expectedPrefix == null) return false;
        if (actual.length < expectedPrefix.length) return false;
        for (int i = 0; i < expectedPrefix.length; i++) {
            if (actual[i] != expectedPrefix[i]) return false;
        }
        return true;
    }

    private static String ltrim(String s) {
        if (s == null || s.isEmpty()) return "";
        int i = 0;
        while (i < s.length()) {
            char c = s.charAt(i);
            if (c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '\uFEFF') {
                i++;
                continue;
            }
            break;
        }
        return s.substring(i);
    }
}
