package com.zioinfo.kintex.auth.profile;

import com.zioinfo.kintex.auth.mapper.UserMapper;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;

/**
 * 프로필 사진 업로드/서빙 서비스 — 모바일 내정보 아바타 계약.
 *
 * <p>저장 컨벤션은 로그인 슬라이드와 동일(파일시스템 {@code {kintex.upload.dir}/avatars/} + 서버 UUID 파일명).
 * 단, 서빙은 정적 /uploads 가 아니라 인증 엔드포인트({@code GET /api/auth/users/{id}/photo})로 바이트 스트림한다
 * (RN {@code <Image>} 가 Authorization 헤더를 직접 부여하는 계약). photo_path 는 서버 UUID 만 저장 —
 * 원본 파일명·클라이언트 경로는 신뢰하지 않으므로 경로 순회는 구조적으로 불가능하다.
 *
 * <p>검증(다층): ①콘텐츠타입 화이트리스트 ②크기 상한 ③매직바이트(실제 파일 시그니처) — 확장자/헤더 위조로
 * SVG·실행형(스크립트) 업로드를 차단한다.
 */
@Service
public class ProfilePhotoService {

    private static final Logger log = LoggerFactory.getLogger(ProfilePhotoService.class);

    /** 아바타 크기 상한(5MB) — 모바일은 1:1 크롭·quality 0.7로 축소 전송. */
    private static final long MAX_IMAGE_BYTES = 5L * 1024 * 1024;

    /** 허용 MIME → 확장자. SVG·gif 등은 화이트리스트 밖(실행형/애니 차단). */
    private static final Map<String, String> ALLOWED_TYPES = Map.of(
            "image/png", "png",
            "image/jpeg", "jpg",
            "image/webp", "webp");

    private final UserMapper userMapper;
    private final Path avatarDir;

    public ProfilePhotoService(UserMapper userMapper,
                               @Value("${kintex.upload.dir:./data/uploads}") String uploadDir) {
        this.userMapper = userMapper;
        this.avatarDir = Paths.get(uploadDir).toAbsolutePath().normalize().resolve("avatars");
    }

    /** 서빙 URL(캐시버스터는 모바일이 부착). {@code GET /api/auth/users/{userId}/photo} 와 동일 경로. */
    public static String serveUrl(String userId) {
        return "/api/auth/users/" + userId + "/photo";
    }

    // ── 업로드(멀티파트) ──────────────────────────────────────────────

    /** 본인 프로필 사진 업로드 → 서빙 URL 반환. userId 는 principal 에서만 온다(본인 강제). */
    public String upload(String userId, MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new ApiException(ErrorCode.VALIDATION, "이미지 파일을 첨부해 주세요.");
        }
        byte[] bytes;
        try {
            bytes = file.getBytes();
        } catch (IOException e) {
            log.warn("avatar read failed: {}", e.getMessage());
            throw new ApiException(ErrorCode.VALIDATION, "이미지를 읽을 수 없습니다.");
        }
        return storeBytes(userId, bytes, file.getContentType());
    }

    // ── 업로드(base64 JSON 대체안) ───────────────────────────────────

    /** base64 페이로드 업로드(멀티파트 곤란 단말 대체). data-URL prefix 는 서비스에서 제거한다. */
    public String uploadBase64(String userId, String base64, String mimeType) {
        if (base64 == null || base64.isBlank()) {
            throw new ApiException(ErrorCode.VALIDATION, "이미지 데이터를 전달해 주세요.");
        }
        String payload = base64;
        int comma = payload.indexOf(','); // "data:image/png;base64,...." 형태 허용
        if (payload.startsWith("data:") && comma > 0) {
            payload = payload.substring(comma + 1);
        }
        byte[] bytes;
        try {
            bytes = java.util.Base64.getDecoder().decode(payload.trim());
        } catch (IllegalArgumentException e) {
            throw new ApiException(ErrorCode.VALIDATION, "이미지 데이터 형식이 올바르지 않습니다.");
        }
        return storeBytes(userId, bytes, mimeType);
    }

    private String storeBytes(String userId, byte[] bytes, String declaredType) {
        if (bytes.length == 0) {
            throw new ApiException(ErrorCode.VALIDATION, "빈 이미지입니다.");
        }
        if (bytes.length > MAX_IMAGE_BYTES) {
            throw new ApiException(ErrorCode.VALIDATION, "이미지는 5MB 이하만 업로드할 수 있습니다.");
        }
        // 매직바이트로 실제 유형 확정(선언 타입은 참고만) — SVG/스크립트/위조 헤더 차단.
        String detected = detectImageType(bytes);
        if (detected == null) {
            throw new ApiException(ErrorCode.VALIDATION, "PNG/JPG/WebP 이미지만 업로드할 수 있습니다.");
        }
        // 선언 타입이 있으면 화이트리스트 여부만 확인(불일치는 매직바이트를 신뢰).
        String normalizedDeclared = declaredType == null ? null : declaredType.toLowerCase(Locale.ROOT).trim();
        if (normalizedDeclared != null && !normalizedDeclared.isBlank()
                && !ALLOWED_TYPES.containsKey(normalizedDeclared)) {
            throw new ApiException(ErrorCode.VALIDATION, "PNG/JPG/WebP 이미지만 업로드할 수 있습니다.");
        }

        String contentType = mimeFor(detected);
        String fileName = UUID.randomUUID() + "." + detected;
        try {
            Files.createDirectories(avatarDir);
            Path target = avatarDir.resolve(fileName).normalize();
            if (!target.startsWith(avatarDir)) { // 파일명이 UUID라 사실상 불변식이나 방어적 확인
                throw new ApiException(ErrorCode.VALIDATION, "잘못된 파일 경로입니다.");
            }
            Files.write(target, bytes);
            // 이전 사진 정리(교체) — 실패는 무해(로그만).
            Map<String, Object> prev = userMapper.findPhotoMeta(userId);
            userMapper.updatePhoto(userId, fileName, contentType);
            if (prev != null) {
                removeQuietly(str(prev.get("photoPath")));
            }
        } catch (IOException e) {
            log.error("avatar store failed for user (masked): {}", e.getMessage());
            throw new ApiException(ErrorCode.INTERNAL, "이미지 저장에 실패했습니다.");
        }
        return serveUrl(userId);
    }

    // ── 서빙 ─────────────────────────────────────────────────────────

    /** 사진 바이트 + Content-Type. 없으면 404(모바일 이니셜 폴백). */
    public StoredPhoto load(String userId) {
        Map<String, Object> meta = userMapper.findPhotoMeta(userId);
        if (meta == null) {
            throw new ApiException(ErrorCode.NOT_FOUND, "등록된 프로필 사진이 없습니다.");
        }
        String fileName = str(meta.get("photoPath"));
        if (fileName == null || fileName.isBlank() || !isSafeName(fileName)) {
            throw new ApiException(ErrorCode.NOT_FOUND, "등록된 프로필 사진이 없습니다.");
        }
        Path target = avatarDir.resolve(fileName).normalize();
        if (!target.startsWith(avatarDir) || !Files.isRegularFile(target)) {
            throw new ApiException(ErrorCode.NOT_FOUND, "등록된 프로필 사진이 없습니다.");
        }
        try {
            byte[] data = Files.readAllBytes(target);
            String ct = str(meta.get("photoContentType"));
            return new StoredPhoto(data, ct == null || ct.isBlank() ? "application/octet-stream" : ct);
        } catch (IOException e) {
            log.warn("avatar load failed: {}", e.getMessage());
            throw new ApiException(ErrorCode.NOT_FOUND, "등록된 프로필 사진이 없습니다.");
        }
    }

    // ── 검증 헬퍼 ────────────────────────────────────────────────────

    /** 매직바이트로 png/jpg/webp 판별. 아니면 null(SVG·스크립트·위조 헤더 차단). 순수 정적 헬퍼. */
    public static String detectImageType(byte[] b) {
        if (b.length >= 8 && (b[0] & 0xFF) == 0x89 && b[1] == 0x50 && b[2] == 0x4E && b[3] == 0x47
                && b[4] == 0x0D && b[5] == 0x0A && b[6] == 0x1A && b[7] == 0x0A) {
            return "png";
        }
        if (b.length >= 3 && (b[0] & 0xFF) == 0xFF && (b[1] & 0xFF) == 0xD8 && (b[2] & 0xFF) == 0xFF) {
            return "jpg";
        }
        // WEBP: "RIFF"...."WEBP"
        if (b.length >= 12 && b[0] == 'R' && b[1] == 'I' && b[2] == 'F' && b[3] == 'F'
                && b[8] == 'W' && b[9] == 'E' && b[10] == 'B' && b[11] == 'P') {
            return "webp";
        }
        return null;
    }

    private static String mimeFor(String ext) {
        return switch (ext) {
            case "png" -> "image/png";
            case "webp" -> "image/webp";
            default -> "image/jpeg";
        };
    }

    private static boolean isSafeName(String name) {
        return !name.contains("/") && !name.contains("\\") && !name.contains("..");
    }

    private void removeQuietly(String fileName) {
        if (fileName == null || fileName.isBlank() || !isSafeName(fileName)) {
            return;
        }
        try {
            Files.deleteIfExists(avatarDir.resolve(fileName));
        } catch (IOException e) {
            log.warn("avatar cleanup failed: {}", e.getMessage());
        }
    }

    private static String str(Object o) {
        return o == null ? null : String.valueOf(o);
    }

    /** 서빙 바이트 컨테이너. */
    public record StoredPhoto(byte[] bytes, String contentType) {
        public StoredPhoto {
            bytes = bytes == null ? new byte[0] : Arrays.copyOf(bytes, bytes.length);
        }
        public byte[] bytes() {
            return Arrays.copyOf(bytes, bytes.length);
        }
    }
}
