package com.tasteby.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.oracle.bmc.ConfigFileReader;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
import com.oracle.bmc.generativeaiinference.model.*;
import com.oracle.bmc.generativeaiinference.requests.ChatRequest;
import com.oracle.bmc.generativeaiinference.requests.EmbedTextRequest;
import com.oracle.bmc.generativeaiinference.responses.ChatResponse;
import com.oracle.bmc.generativeaiinference.responses.EmbedTextResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Service
public class OciGenAiService {

    private static final Logger log = LoggerFactory.getLogger(OciGenAiService.class);
    private static final int EMBED_BATCH_SIZE = 96;

    @Value("${app.oci.compartment-id}")
    private String compartmentId;

    @Value("${app.oci.chat-endpoint}")
    private String chatEndpoint;

    @Value("${app.oci.embed-endpoint}")
    private String embedEndpoint;

    @Value("${app.oci.chat-model-id}")
    private String chatModelId;

    @Value("${app.oci.embed-model-id}")
    private String embedModelId;

    private final ObjectMapper mapper;
    private ConfigFileAuthenticationDetailsProvider authProvider;
    private GenerativeAiInferenceClient chatClient;
    private GenerativeAiInferenceClient embedClient;

    public OciGenAiService(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    @PostConstruct
    public void init() {
        try {
            ConfigFileReader.ConfigFile configFile = ConfigFileReader.parseDefault();
            authProvider = new ConfigFileAuthenticationDetailsProvider(configFile);
            chatClient = GenerativeAiInferenceClient.builder()
                    .endpoint(chatEndpoint).build(authProvider);
            embedClient = GenerativeAiInferenceClient.builder()
                    .endpoint(embedEndpoint).build(authProvider);
            log.info("OCI GenAI auth configured (clients initialized)");
        } catch (Exception e) {
            log.warn("OCI config not found, GenAI features disabled: {}", e.getMessage());
        }
    }

    @PreDestroy
    public void destroy() {
        if (chatClient != null) chatClient.close();
        if (embedClient != null) embedClient.close();
    }

    /**
     * Call OCI GenAI LLM (Chat).
     */
    public String chat(String prompt, int maxTokens) {
        if (chatClient == null) throw new IllegalStateException("OCI GenAI not configured");

        var textContent = TextContent.builder().text(prompt).build();
        var userMessage = UserMessage.builder().content(List.of(textContent)).build();

        var chatRequest = GenericChatRequest.builder()
                .messages(List.of(userMessage))
                .maxTokens(maxTokens)
                .temperature(0.0)
                .build();

        var chatDetails = ChatDetails.builder()
                .compartmentId(compartmentId)
                .servingMode(OnDemandServingMode.builder().modelId(chatModelId).build())
                .chatRequest(chatRequest)
                .build();

        ChatResponse response = chatClient.chat(
                ChatRequest.builder().chatDetails(chatDetails).build());

        var chatResult = (GenericChatResponse) response.getChatResult().getChatResponse();
        var choice = chatResult.getChoices().get(0);
        var content = ((TextContent) choice.getMessage().getContent().get(0)).getText();
        return content.trim();
    }

    /**
     * Generate embeddings for a list of texts.
     */
    public List<List<Double>> embedTexts(List<String> texts) {
        if (authProvider == null) throw new IllegalStateException("OCI GenAI not configured");

        List<List<Double>> allEmbeddings = new ArrayList<>();
        for (int i = 0; i < texts.size(); i += EMBED_BATCH_SIZE) {
            List<String> batch = texts.subList(i, Math.min(i + EMBED_BATCH_SIZE, texts.size()));
            allEmbeddings.addAll(embedBatch(batch));
        }
        return allEmbeddings;
    }

    private List<List<Double>> embedBatch(List<String> texts) {
        if (embedClient == null) throw new IllegalStateException("OCI GenAI not configured");

        var embedDetails = EmbedTextDetails.builder()
                .inputs(texts)
                .servingMode(OnDemandServingMode.builder().modelId(embedModelId).build())
                .compartmentId(compartmentId)
                .inputType(EmbedTextDetails.InputType.SearchDocument)
                .build();

        EmbedTextResponse response = embedClient.embedText(
                EmbedTextRequest.builder().embedTextDetails(embedDetails).build());

        return response.getEmbedTextResult().getEmbeddings()
                .stream()
                .map(emb -> emb.stream().map(Number::doubleValue).toList())
                .toList();
    }

    /**
     * Parse LLM response as JSON (handles markdown code blocks, truncated arrays, etc.)
     */
    public Object parseJson(String raw) {
        // Strip markdown code blocks
        raw = raw.replaceAll("(?m)^```(?:json)?\\s*|\\s*```$", "").trim();
        // Remove trailing commas
        raw = raw.replaceAll(",\\s*([}\\]])", "$1");

        try {
            return mapper.readValue(raw, Object.class);
        } catch (Exception ignored) {}

        // #326 — Recover truncated array. Brace depth counter로 단일 패스 O(N).
        // 이전: 각 idx에서 end를 1씩 늘려가며 매번 readValue → O(N²) + 예외 스택트레이스 양산.
        if (raw.trim().startsWith("[")) {
            List<Object> items = new ArrayList<>();
            int idx = raw.indexOf('[') + 1;
            while (idx < raw.length()) {
                while (idx < raw.length() && " \t\n\r,".indexOf(raw.charAt(idx)) >= 0) idx++;
                if (idx >= raw.length() || raw.charAt(idx) == ']') break;
                if (raw.charAt(idx) != '{') break;  // 객체 시작이 아니면 복구 중단

                int end = findObjectEnd(raw, idx);
                if (end < 0) break;  // 잘린 객체 — 거기서 멈춤
                try {
                    Object obj = mapper.readValue(raw.substring(idx, end + 1), Object.class);
                    items.add(obj);
                } catch (Exception ignored2) {
                    break;  // 불가해 객체 — 멈춤
                }
                idx = end + 1;
            }
            if (!items.isEmpty()) {
                log.info("Recovered {} items from truncated JSON", items.size());
                return items;
            }
        }

        throw new RuntimeException("JSON parse failed: " + raw.substring(0, Math.min(80, raw.length())));
    }

    /**
     * #326 — JSON 객체 시작 위치(`{`)에서 매칭되는 닫는 `}` 인덱스를 반환.
     * 문자열 안의 `{` `}`와 escape는 무시. 매칭 못 찾으면 -1.
     */
    private static int findObjectEnd(String raw, int start) {
        int depth = 0;
        boolean inString = false;
        boolean escaped = false;
        for (int i = start; i < raw.length(); i++) {
            char c = raw.charAt(i);
            if (escaped) { escaped = false; continue; }
            if (c == '\\') { escaped = true; continue; }
            if (c == '"') { inString = !inString; continue; }
            if (inString) continue;
            if (c == '{') depth++;
            else if (c == '}') {
                depth--;
                if (depth == 0) return i;
            }
        }
        return -1;
    }
}
