package com.zioinfo.kintex.work.meeting;

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.util.Set;
import java.util.UUID;

/**
 * 회의 음성 임시 저장소(UIWS meeting 패턴 이식). STT 시도 후 폐기(개인정보 최소화 — DB BLOB 금지).
 *
 * <p>토큰(audioId)은 UUID(파일명 추측·경로순회 차단). 실제 경로는 서버에서만 해석하며 물리 파일명은
 * 응답/로그에 노출하지 않는다. 저장 루트는 첨부 루트와 분리한다({@code {kintex.upload.dir}/meeting-tmp}).
 *
 * <p>검증: ①빈 파일 거부 ②크기 상한(20MB) ③오디오 계열 확장자/콘텐츠타입 화이트리스트.
 */
@Service
public class MeetingAudioStore {

    private static final Logger log = LoggerFactory.getLogger(MeetingAudioStore.class);
    private static final String SUBDIR = "meeting-tmp";

    /** 회의 녹음 상한(20MB) — 장시간 녹음은 분할 업로드 권장. */
    private static final long MAX_AUDIO_BYTES = 20L * 1024 * 1024;

    /** 오디오 계열 확장자 화이트리스트(실행형/문서 업로드 차단). */
    private static final Set<String> ALLOWED_EXT =
            Set.of("m4a", "mp3", "mp4", "wav", "webm", "ogg", "aac", "amr", "3gp", "caf", "flac");

    private final Path root;

    public MeetingAudioStore(@Value("${kintex.upload.dir:./data/uploads}") String uploadDir) {
        this.root = Paths.get(uploadDir).toAbsolutePath().normalize();
    }

    /** 멀티파트 오디오를 검증·임시 저장하고 audioId(UUID 토큰) 반환. */
    public String store(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new ApiException(ErrorCode.VALIDATION, "녹음 파일을 첨부해 주세요.");
        }
        if (file.getSize() > MAX_AUDIO_BYTES) {
            throw new ApiException(ErrorCode.VALIDATION, "녹음 파일은 20MB 이하만 업로드할 수 있습니다.");
        }
        validateFormat(file);
        String token = UUID.randomUUID().toString().replace("-", "");
        try {
            Path target = resolve(token);
            Files.createDirectories(target.getParent());
            file.transferTo(target.toFile());
            return token;
        } catch (IOException e) {
            log.warn("meeting audio store failed: {}", e.getClass().getSimpleName());
            throw new ApiException(ErrorCode.INTERNAL, "녹음 파일 저장에 실패했습니다.");
        }
    }

    /** 토큰으로 임시 파일 경로 해석(경로순회 차단). 존재 보장은 하지 않음. */
    public Path path(String audioId) {
        return resolve(sanitize(audioId));
    }

    /** 임시 파일 폐기(전사 완료/실패 무관 best-effort). */
    public void discard(String audioId) {
        if (audioId == null || audioId.isBlank()) {
            return;
        }
        try {
            Files.deleteIfExists(resolve(sanitize(audioId)));
        } catch (Exception ignore) {
            // 임시파일 삭제 실패는 무시(정합성 영향 없음)
        }
    }

    private void validateFormat(MultipartFile file) {
        String name = file.getOriginalFilename();
        String ext = null;
        if (name != null) {
            int dot = name.lastIndexOf('.');
            if (dot >= 0 && dot < name.length() - 1) {
                ext = name.substring(dot + 1).toLowerCase();
            }
        }
        String ct = file.getContentType();
        boolean ctAudio = ct != null && (ct.startsWith("audio/") || ct.startsWith("video/"));
        boolean extOk = ext != null && ALLOWED_EXT.contains(ext);
        // 확장자 화이트리스트 우선, 없으면 audio/* 콘텐츠타입 허용(모바일 녹음은 확장자 누락 가능).
        if (!extOk && !ctAudio) {
            throw new ApiException(ErrorCode.VALIDATION, "지원하지 않는 오디오 형식입니다(m4a·mp3·wav·webm 등).");
        }
    }

    private Path resolve(String token) {
        Path base = root.resolve(SUBDIR);
        Path target = base.resolve(token + ".audio").normalize();
        if (!target.startsWith(base)) {
            throw new ApiException(ErrorCode.VALIDATION, "허용되지 않은 경로입니다.");
        }
        return target;
    }

    /** 토큰은 영숫자만 허용(경로순회·확장자 주입 차단). */
    private String sanitize(String token) {
        if (token == null || !token.matches("[A-Za-z0-9]{1,64}")) {
            throw new ApiException(ErrorCode.NOT_FOUND, "녹음 파일을 찾을 수 없습니다.");
        }
        return token;
    }
}
