package com.pandoli365.bibimbap.controller.api;

import com.pandoli365.bibimbap.security.CsrfTokens;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@RestController
@RequestMapping("/api/game-files")
public class GameUploadController {

    private static final Logger log = LoggerFactory.getLogger(GameUploadController.class);
    private static final long WEBGL_EXTRACTED_MAX_BYTES = 512L * 1024 * 1024;
    private static final int WEBGL_MAX_ENTRIES = 8_000;
    private static final long THUMBNAIL_MAX_BYTES = 10L * 1024 * 1024;

    @Value("${app.upload.game-storage-path:src/main/resources/static}")
    private String uploadStoragePath;

    @GetMapping("/ping")
    public ResponseEntity<Map<String, Object>> ping() {
        return ResponseEntity.ok(Map.of("status", "ok"));
    }

    @PostMapping
    public ResponseEntity<Map<String, Object>> uploadGameFiles(
            @RequestParam("files") MultipartFile[] files,
            @RequestParam(name = "path", required = false) String path,
            HttpServletRequest request,
            HttpSession session
    ) throws IOException {
        if (!CsrfTokens.isValid(request)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(CsrfTokens.errorBody());
        }
        if (sessionUserId(session) == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(Map.of("message", "login is required"));
        }
        if (files == null || files.length == 0) {
            return ResponseEntity.badRequest().body(Map.of("message", "files is required"));
        }

        Path root = gameRoot();
        List<Map<String, String>> uploadedFiles = new ArrayList<>();
        for (MultipartFile file : files) {
            if (file == null || file.isEmpty()) {
                continue;
            }

            Path targetFile;
            try {
                targetFile = resolveTargetFile(root, path, file, files.length);
            } catch (IllegalArgumentException e) {
                return ResponseEntity.badRequest().body(Map.of("message", "invalid path"));
            }

            Files.createDirectories(targetFile.getParent());
            Files.copy(file.getInputStream(), targetFile, StandardCopyOption.REPLACE_EXISTING);
            String relativePath = root.relativize(targetFile).toString().replace('\\', '/');
            uploadedFiles.add(Map.of(
                    "fileName", targetFile.getFileName().toString(),
                    "path", relativePath,
                    "url", "/game/" + relativePath
            ));
        }

        Map<String, Object> response = new LinkedHashMap<>();
        response.put("storagePath", root.toString());
        response.put("files", uploadedFiles);
        return ResponseEntity.ok(response);
    }

    @PostMapping("/webgl-zip")
    public ResponseEntity<Map<String, Object>> uploadWebglZip(
            @RequestParam(name = "file", required = false) MultipartFile file,
            HttpServletRequest request,
            HttpSession session
    ) throws IOException {
        if (!CsrfTokens.isValid(request)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(CsrfTokens.errorBody());
        }
        log.info("WebGL zip upload request received. fileName={}, size={}, contentType={}",
                file == null ? null : file.getOriginalFilename(),
                file == null ? null : file.getSize(),
                file == null ? null : file.getContentType());
        Long userId = sessionUserId(session);
        if (userId == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(Map.of("message", "로그인이 필요합니다."));
        }
        if (file == null || file.isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("message", "WebGL zip 파일을 선택해 주세요."));
        }
        if (!isZipFile(file)) {
            return ResponseEntity.badRequest().body(Map.of("message", "zip 파일만 업로드할 수 있습니다."));
        }

        Path root = gameRoot();
        String gameUuid = UUID.randomUUID().toString();
        Path targetDir = root.resolve(gameUuid).normalize();
        if (!targetDir.startsWith(root)) {
            return ResponseEntity.badRequest().body(Map.of("message", "저장 경로가 올바르지 않습니다."));
        }

        try {
            Files.createDirectories(targetDir);
            ExtractResult extractResult = extractZip(file, targetDir);
            Path indexFile = findIndexFile(targetDir);
            if (indexFile == null) {
                deleteRecursively(targetDir);
                return ResponseEntity.badRequest().body(Map.of("message", "zip 안에서 index.html을 찾지 못했습니다."));
            }

            String webglPath = "/game/" + root.relativize(indexFile).toString().replace('\\', '/');
            String deployPath = "/game/" + root.relativize(targetDir).toString().replace('\\', '/');
            Map<String, Object> response = new LinkedHashMap<>();
            response.put("status", 200);
            response.put("message", "WebGL 파일이 압축 해제되었습니다.");
            response.put("gameUuid", gameUuid);
            response.put("webglPath", webglPath);
            response.put("deployPath", deployPath);
            response.put("entryCount", extractResult.entryCount());
            response.put("extractedBytes", extractResult.extractedBytes());
            log.info("WebGL zip upload completed. gameUuid={}, webglPath={}, entryCount={}, extractedBytes={}",
                    gameUuid, webglPath, extractResult.entryCount(), extractResult.extractedBytes());
            return ResponseEntity.ok(response);
        } catch (IllegalArgumentException e) {
            deleteRecursively(targetDir);
            return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
        } catch (IOException e) {
            deleteRecursively(targetDir);
            throw e;
        }
    }

    @PostMapping("/thumbnail")
    public ResponseEntity<Map<String, Object>> uploadThumbnail(
            @RequestParam(name = "file", required = false) MultipartFile file,
            @RequestParam(name = "gameUuid", required = false) String gameUuid,
            HttpServletRequest request,
            HttpSession session
    ) throws IOException {
        if (!CsrfTokens.isValid(request)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(CsrfTokens.errorBody());
        }
        Long userId = sessionUserId(session);
        if (userId == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(Map.of("message", "로그인이 필요합니다."));
        }
        if (file == null || file.isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("message", "썸네일 이미지를 선택해 주세요."));
        }
        if (file.getSize() > THUMBNAIL_MAX_BYTES) {
            return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
                    .body(Map.of("message", "썸네일 이미지는 10MB 이하로 올려 주세요."));
        }

        String extension = imageExtension(file);
        if (extension == null) {
            return ResponseEntity.badRequest()
                    .body(Map.of("message", "PNG, JPG, WEBP, GIF 이미지만 사용할 수 있습니다."));
        }

        String normalizedGameUuid = normalizeGameUuid(gameUuid);
        if (normalizedGameUuid == null) {
            return ResponseEntity.badRequest().body(Map.of("message", "WebGL zip을 먼저 업로드해 주세요."));
        }

        Path root = gameRoot();
        Path targetDir = root.resolve(normalizedGameUuid).normalize();
        Path targetFile = targetDir.resolve("thumbnail" + extension).normalize();
        if (!targetDir.startsWith(root) || !targetFile.startsWith(targetDir)) {
            return ResponseEntity.badRequest().body(Map.of("message", "저장 경로가 올바르지 않습니다."));
        }
        if (!Files.isDirectory(targetDir)) {
            return ResponseEntity.badRequest().body(Map.of("message", "WebGL zip을 먼저 업로드해 주세요."));
        }

        deleteExistingThumbnail(targetDir);
        Files.copy(file.getInputStream(), targetFile, StandardCopyOption.REPLACE_EXISTING);
        String thumbnailUrl = "/game/" + root.relativize(targetFile).toString().replace('\\', '/');

        Map<String, Object> response = new LinkedHashMap<>();
        response.put("status", 200);
        response.put("message", "썸네일 이미지가 저장되었습니다.");
        response.put("thumbnailUrl", thumbnailUrl);
        return ResponseEntity.ok(response);
    }

    private Path gameRoot() {
        return Paths.get(uploadStoragePath).toAbsolutePath().normalize().resolve("game").normalize();
    }

    private Long sessionUserId(HttpSession session) {
        if (session == null) {
            return null;
        }
        Object userId = session.getAttribute("userId");
        if (userId instanceof Number number) {
            return number.longValue();
        }
        if (userId instanceof String text) {
            try {
                return Long.parseLong(text);
            } catch (NumberFormatException e) {
                return null;
            }
        }
        return null;
    }

    private boolean isZipFile(MultipartFile file) {
        String contentType = file.getContentType();
        if (contentType != null) {
            String normalized = contentType.toLowerCase(Locale.ROOT);
            if ("application/zip".equals(normalized)
                    || "application/x-zip-compressed".equals(normalized)
                    || "multipart/x-zip".equals(normalized)) {
                return true;
            }
        }
        String originalName = file.getOriginalFilename();
        return originalName != null && originalName.toLowerCase(Locale.ROOT).endsWith(".zip");
    }

    private ExtractResult extractZip(MultipartFile zipFile, Path targetDir) throws IOException {
        long extractedBytes = 0;
        int entryCount = 0;

        try (ZipInputStream zipInput = new ZipInputStream(zipFile.getInputStream())) {
            ZipEntry entry;
            while ((entry = zipInput.getNextEntry()) != null) {
                entryCount++;
                if (entryCount > WEBGL_MAX_ENTRIES) {
                    throw new IllegalArgumentException("zip 안의 파일 수가 너무 많습니다.");
                }

                Path target = targetDir.resolve(entry.getName()).normalize();
                if (!target.startsWith(targetDir)) {
                    throw new IllegalArgumentException("zip 안에 올바르지 않은 경로가 포함되어 있습니다.");
                }

                if (entry.isDirectory()) {
                    Files.createDirectories(target);
                } else {
                    if (target.getParent() == null) {
                        throw new IllegalArgumentException("zip 안에 올바르지 않은 파일이 포함되어 있습니다.");
                    }
                    Files.createDirectories(target.getParent());
                    extractedBytes += copyZipEntry(zipInput, target, extractedBytes);
                }
                zipInput.closeEntry();
            }
        }

        if (entryCount == 0) {
            throw new IllegalArgumentException("비어 있는 zip 파일입니다.");
        }
        return new ExtractResult(entryCount, extractedBytes);
    }

    private long copyZipEntry(InputStream input, Path target, long bytesBeforeEntry) throws IOException {
        byte[] buffer = new byte[8192];
        long copied = 0;
        try (var output = Files.newOutputStream(target)) {
            int read;
            while ((read = input.read(buffer)) != -1) {
                copied += read;
                if (bytesBeforeEntry + copied > WEBGL_EXTRACTED_MAX_BYTES) {
                    throw new IllegalArgumentException("압축 해제된 파일 크기가 너무 큽니다.");
                }
                output.write(buffer, 0, read);
            }
        }
        return copied;
    }

    private Path findIndexFile(Path targetDir) throws IOException {
        try (Stream<Path> paths = Files.walk(targetDir)) {
            return paths
                    .filter(Files::isRegularFile)
                    .filter(path -> "index.html".equalsIgnoreCase(path.getFileName().toString()))
                    .min(Comparator
                            .comparingInt((Path path) -> targetDir.relativize(path).getNameCount())
                            .thenComparing(path -> targetDir.relativize(path).toString()))
                    .orElse(null);
        }
    }

    private String imageExtension(MultipartFile file) {
        String contentType = file.getContentType();
        if (contentType != null) {
            switch (contentType.toLowerCase(Locale.ROOT)) {
                case "image/png":
                    return ".png";
                case "image/jpeg":
                case "image/jpg":
                    return ".jpg";
                case "image/webp":
                    return ".webp";
                case "image/gif":
                    return ".gif";
                default:
                    break;
            }
        }

        String originalName = file.getOriginalFilename();
        if (originalName == null) {
            return null;
        }
        String cleanName = Paths.get(originalName).getFileName().toString().toLowerCase(Locale.ROOT);
        if (cleanName.endsWith(".png")) {
            return ".png";
        }
        if (cleanName.endsWith(".jpg") || cleanName.endsWith(".jpeg")) {
            return ".jpg";
        }
        if (cleanName.endsWith(".webp")) {
            return ".webp";
        }
        if (cleanName.endsWith(".gif")) {
            return ".gif";
        }
        return null;
    }

    private String normalizeGameUuid(String gameUuid) {
        if (gameUuid == null || gameUuid.isBlank()) {
            return null;
        }
        try {
            return UUID.fromString(gameUuid.trim()).toString();
        } catch (IllegalArgumentException e) {
            return null;
        }
    }

    private void deleteExistingThumbnail(Path targetDir) throws IOException {
        try (Stream<Path> paths = Files.list(targetDir)) {
            for (Path path : paths.toList()) {
                String fileName = path.getFileName().toString().toLowerCase(Locale.ROOT);
                if (Files.isRegularFile(path) && isThumbnailFile(fileName)) {
                    Files.deleteIfExists(path);
                }
            }
        }
    }

    private boolean isThumbnailFile(String fileName) {
        return fileName.equals("thumbnail.png")
                || fileName.equals("thumbnail.jpg")
                || fileName.equals("thumbnail.webp")
                || fileName.equals("thumbnail.gif");
    }

    private void deleteRecursively(Path path) throws IOException {
        if (!Files.exists(path)) {
            return;
        }
        try (Stream<Path> paths = Files.walk(path)) {
            for (Path target : paths.sorted(Comparator.reverseOrder()).toList()) {
                Files.deleteIfExists(target);
            }
        }
    }

    private Path resolveTargetFile(Path root, String path, MultipartFile file, int fileCount) {
        String safePath = path == null ? "" : path.trim().replace('\\', '/');
        while (safePath.startsWith("/")) {
            safePath = safePath.substring(1);
        }

        boolean usePathAsDirectory = safePath.isBlank() || safePath.endsWith("/") || fileCount > 1;
        Path targetFile = usePathAsDirectory
                ? root.resolve(safePath).resolve(uniqueFileName(file.getOriginalFilename()))
                : root.resolve(safePath);
        targetFile = targetFile.normalize();
        if (!targetFile.startsWith(root) || targetFile.getParent() == null) {
            throw new IllegalArgumentException("invalid path");
        }
        return targetFile;
    }

    private String uniqueFileName(String originalFileName) {
        String cleanName = originalFileName == null ? "file" : Paths.get(originalFileName).getFileName().toString();
        cleanName = cleanName.replaceAll("[^A-Za-z0-9._-]", "_");
        if (cleanName.isBlank() || ".".equals(cleanName) || "..".equals(cleanName)) {
            cleanName = "file";
        }
        return UUID.randomUUID() + "_" + cleanName;
    }

    private record ExtractResult(int entryCount, long extractedBytes) {
    }
}
