package com.zioinfo.kintex.work.meeting;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * 회의 녹음 STT 클라이언트(UIWS meeting 패턴 이식) — 온프레미스 whisper.cpp(선택) → degraded.
 *
 * <p><b>서버 RAM 제약</b>: 대형 STT 모델(whisper large 등)을 이 서버에서 직접 구동하지 않는다.
 * whisper 엔드포인트가 <b>env 로 설정된 경우에만</b>(별도 온프레미스/원격 whisper 서비스) 시도하고,
 * 미설정(기본) 시 즉시 degraded 폴백(빈 전사) → 사용자가 전사 텍스트를 직접 입력한다.
 * CLOVA 등 외부 STT 는 킨텍스 승인 범위 밖이므로 사용하지 않는다(외부 API = Claude 텍스트만 허용).
 *
 * <p>동시 STT 1건 제한(부하 억제). 예외는 상위로 전파하지 않으며(무전파) 로그에는 상태코드만 남긴다.
 * <b>보안 불변</b>: 키·오디오·전사 원문을 로그·에러·응답에 미기재. 오디오 임시 파일 폐기는 호출자 책임.
 */
@Service
public class MeetingSttClient {

    private static final Logger log = LoggerFactory.getLogger(MeetingSttClient.class);
    private static final Semaphore GATE = new Semaphore(1);
    private static final ObjectMapper MAPPER = new ObjectMapper();

    private final String whisperUrl;
    private final String whisperKey;
    private final int timeoutSec;

    public MeetingSttClient(
            @Value("${kintex.meeting.stt.whisper.url:}") String whisperUrl,
            @Value("${kintex.meeting.stt.whisper.key:}") String whisperKey,
            @Value("${kintex.meeting.stt.whisper.timeout-sec:90}") int timeoutSec) {
        this.whisperUrl = whisperUrl == null ? "" : whisperUrl.trim();
        this.whisperKey = whisperKey == null ? "" : whisperKey.trim();
        this.timeoutSec = timeoutSec <= 0 ? 90 : timeoutSec;
    }

    /** STT 결과(degraded 시 transcript 는 빈 문자열). */
    public record SttResult(String transcript, boolean degraded) {
    }

    private boolean enabled() {
        return !whisperUrl.isBlank();
    }

    /**
     * 임시 오디오 파일을 전사한다. whisper 미설정/실패/빈결과/타임아웃/파일없음 → degraded(예외 무전파).
     * @param audioPath 서버 임시 파일시스템 경로(전사 후 폐기 책임은 호출자)
     */
    public SttResult transcribe(Path audioPath) {
        if (!enabled()) {
            log.info("STT disabled (whisper 미설정) → degraded fallback");
            return new SttResult("", true);
        }
        if (audioPath == null || !Files.exists(audioPath)) {
            return new SttResult("", true);
        }
        boolean acquired = false;
        try {
            acquired = GATE.tryAcquire(timeoutSec, TimeUnit.SECONDS);
            if (!acquired) {
                log.warn("STT busy (concurrency gate timeout) → degraded fallback");
                return new SttResult("", true);
            }
            byte[] audio = Files.readAllBytes(audioPath);
            String transcript = tryWhisper(audio);
            if (transcript != null && !transcript.isBlank()) {
                return new SttResult(transcript.trim(), false);
            }
            return new SttResult("", true);
        } catch (Exception e) {
            log.warn("STT failed → degraded fallback: {}", e.getClass().getSimpleName());
            return new SttResult("", true);
        } finally {
            if (acquired) {
                GATE.release();
            }
        }
    }

    /** whisper.cpp 온프레미스 전사 시도. multipart file=오디오 + response_format=json, 헤더 X-Whisper-Key. */
    private String tryWhisper(byte[] audio) {
        try {
            String boundary = "----kintexWhisper" + UUID.randomUUID().toString().replace("-", "");
            byte[] body = buildMultipart(boundary, audio);

            HttpClient.Builder cb = HttpClient.newBuilder()
                    .connectTimeout(Duration.ofSeconds(Math.min(15, timeoutSec)));
            HttpRequest.Builder rb = HttpRequest.newBuilder()
                    .uri(URI.create(whisperUrl))
                    .timeout(Duration.ofSeconds(timeoutSec))
                    .header("Content-Type", "multipart/form-data; boundary=" + boundary)
                    .header("Accept", "application/json")
                    .POST(HttpRequest.BodyPublishers.ofByteArray(body));
            if (!whisperKey.isBlank()) {
                rb.header("X-Whisper-Key", whisperKey);
            }
            HttpResponse<String> resp = cb.build().send(rb.build(), HttpResponse.BodyHandlers.ofString());
            if (resp.statusCode() != 200) {
                log.warn("Whisper STT status {} → degraded fallback", resp.statusCode());
                return null;
            }
            return extractText(resp.body());
        } catch (Exception e) {
            log.warn("Whisper STT failed → degraded fallback: {}", e.getClass().getSimpleName());
            return null;
        }
    }

    /** whisper multipart/form-data 본문 수동 구성(신규 의존성 없이 java.net.http). */
    private static byte[] buildMultipart(String boundary, byte[] audio) throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        String dash = "--";
        String crlf = "\r\n";
        out.write((dash + boundary + crlf).getBytes(StandardCharsets.UTF_8));
        out.write(("Content-Disposition: form-data; name=\"file\"; filename=\"audio\"" + crlf)
                .getBytes(StandardCharsets.UTF_8));
        out.write(("Content-Type: application/octet-stream" + crlf + crlf).getBytes(StandardCharsets.UTF_8));
        out.write(audio);
        out.write(crlf.getBytes(StandardCharsets.UTF_8));
        out.write((dash + boundary + crlf).getBytes(StandardCharsets.UTF_8));
        out.write(("Content-Disposition: form-data; name=\"response_format\"" + crlf + crlf)
                .getBytes(StandardCharsets.UTF_8));
        out.write("json".getBytes(StandardCharsets.UTF_8));
        out.write(crlf.getBytes(StandardCharsets.UTF_8));
        out.write((dash + boundary + dash + crlf).getBytes(StandardCharsets.UTF_8));
        return out.toByteArray();
    }

    /** 응답 JSON 에서 전사 text 추출(text 우선, 없으면 segments[].text 이어붙임). 실패 시 null. */
    private static String extractText(String body) {
        if (body == null || body.isBlank()) {
            return null;
        }
        try {
            JsonNode root = MAPPER.readTree(body);
            JsonNode text = root.get("text");
            if (text != null && !text.isNull() && !text.asText().isBlank()) {
                return text.asText();
            }
            JsonNode segments = root.get("segments");
            if (segments != null && segments.isArray() && segments.size() > 0) {
                StringBuilder sb = new StringBuilder();
                for (JsonNode seg : segments) {
                    JsonNode st = seg.get("text");
                    if (st != null && !st.isNull() && !st.asText().isBlank()) {
                        if (sb.length() > 0) {
                            sb.append(' ');
                        }
                        sb.append(st.asText().trim());
                    }
                }
                return sb.length() > 0 ? sb.toString() : null;
            }
            return null;
        } catch (Exception e) {
            return null;
        }
    }
}
