package de.assecutor.votianlt.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.assecutor.votianlt.ai.service.LlmRestClient;
import de.assecutor.votianlt.model.Language;
import de.assecutor.votianlt.model.TranslationCacheEntry;
import de.assecutor.votianlt.repository.TranslationCacheRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * Service for translating text into all available languages. Uses LLM API for
 * translations with MongoDB-backed caching.
 *
 * <p>
 * The central method is {@link #translateBatch(List)}: it checks the cache for
 * every requested text and, for all cache misses, issues exactly ONE LLM call
 * that returns translations for all missing texts and all languages at once.
 */
@Service
@Slf4j
public class TranslationService {

    private static final long CACHE_CLEANUP_THRESHOLD = 100_000L;
    private static final long CACHE_TARGET_SIZE = 50_000L;

    private final LlmRestClient llmRestClient;
    private final TranslationCacheRepository cacheRepository;
    private final ObjectMapper objectMapper;

    public TranslationService(LlmRestClient llmRestClient, TranslationCacheRepository cacheRepository,
            ObjectMapper objectMapper) {
        this.llmRestClient = llmRestClient;
        this.cacheRepository = cacheRepository;
        this.objectMapper = objectMapper;
    }

    /**
     * Represents a translation with language code and text.
     */
    public record Translation(String language, String text) {
    }

    /**
     * Returns all available languages for translation.
     */
    public List<Language> getAvailableLanguages() {
        return Arrays.asList(Language.DE, Language.EN, Language.FR, Language.ES, Language.TR, Language.PL, Language.RU,
                Language.EE, Language.LV, Language.LT);
    }

    /**
     * Translates a list of texts into all available languages using at most one LLM
     * call. Texts already present in the MongoDB cache are served from there;
     * remaining cache misses are translated together in a single LLM request.
     *
     * @param texts
     *            the texts to translate (duplicates and blanks are ignored)
     * @return map from each source text to its list of translations
     */
    public Map<String, List<Translation>> translateBatch(List<String> texts) {
        if (texts == null || texts.isEmpty()) {
            return Map.of();
        }

        Map<String, List<Translation>> result = new LinkedHashMap<>();
        List<String> cacheMisses = new ArrayList<>();

        // Phase 1: serve from cache where possible
        for (String text : texts) {
            if (text == null || text.isBlank()) {
                continue;
            }
            Optional<TranslationCacheEntry> cached = cacheRepository.findBySourceText(text);
            if (cached.isPresent()) {
                log.debug("[TranslationCache] Cache hit for '{}'", text);
                result.put(text, toTranslationList(cached.get().getTranslations()));
            } else {
                cacheMisses.add(text);
            }
        }

        if (cacheMisses.isEmpty()) {
            log.debug("[TranslationCache] All {} texts served from cache", texts.size());
            return result;
        }

        log.debug("[TranslationCache] {} cached, {} cache misses – calling LLM once", result.size(),
                cacheMisses.size());

        // Phase 2: one LLM call for all cache misses
        Map<Integer, Map<String, String>> batchResult = callLlmForBatch(cacheMisses);

        for (int i = 0; i < cacheMisses.size(); i++) {
            String text = cacheMisses.get(i);
            Map<String, String> translationMap = batchResult.getOrDefault(i, Map.of());

            List<Translation> translations = buildTranslationList(text, translationMap);
            result.put(text, translations);

            // Persist in MongoDB cache
            Map<String, String> mapToStore = translationMap.isEmpty() ? toTranslationMap(translations) : translationMap;
            try {
                cacheRepository.save(new TranslationCacheEntry(text, mapToStore));
            } catch (Exception e) {
                log.debug("[TranslationCache] Could not persist cache entry (may be duplicate): {}", e.getMessage());
            }
        }

        log.debug("[TranslationCache] Batch complete – {} texts translated, cache size: {}", cacheMisses.size(),
                cacheRepository.count());
        return result;
    }

    /**
     * Convenience wrapper for a single text. Delegates to
     * {@link #translateBatch(List)}.
     *
     * @param text
     *            the text to translate
     * @return list of translations for all available languages
     */
    public List<Translation> translateToAllLanguages(String text) {
        if (text == null || text.isBlank()) {
            return List.of();
        }
        return translateBatch(List.of(text)).getOrDefault(text, List.of());
    }

    /**
     * Scheduled cleanup: if the cache exceeds 100,000 entries, deletes the oldest
     * entries until only 50,000 remain. Runs every hour.
     */
    @Scheduled(fixedDelay = 3_600_000)
    public void cleanupCacheIfNeeded() {
        long count = cacheRepository.count();
        log.debug("[TranslationCache] Scheduled cleanup check – current size: {}", count);

        if (count > CACHE_CLEANUP_THRESHOLD) {
            long toDelete = count - CACHE_TARGET_SIZE;
            log.info("[TranslationCache] Cache size {} exceeds threshold {}, deleting {} oldest entries", count,
                    CACHE_CLEANUP_THRESHOLD, toDelete);

            List<TranslationCacheEntry> oldest = cacheRepository
                    .findOldestEntries(PageRequest.of(0, (int) toDelete, Sort.by(Sort.Direction.ASC, "inserted_at")));

            cacheRepository.deleteAll(oldest);
            log.info("[TranslationCache] Deleted {} entries, new size: {}", oldest.size(), cacheRepository.count());
        }
    }

    /**
     * Returns the current number of entries in the MongoDB translation cache.
     */
    public long getCacheSize() {
        return cacheRepository.count();
    }

    /**
     * Clears the entire translation cache.
     */
    public void clearCache() {
        cacheRepository.deleteAll();
        log.info("[TranslationCache] Cache cleared");
    }

    // ---------------------------------------------------------------------------
    // private helpers
    // ---------------------------------------------------------------------------

    /**
     * Sends a single LLM request for a list of texts and returns translations
     * indexed by the position in the input list.
     */
    private Map<Integer, Map<String, String>> callLlmForBatch(List<String> texts) {
        StringBuilder textsListing = new StringBuilder();
        for (int i = 0; i < texts.size(); i++) {
            textsListing.append(i).append(": ").append(texts.get(i)).append("\n");
        }

        String systemPrompt = """
                You are a professional translator.
                Translate ALL of the following numbered texts into ALL of these languages and return \
                the result as a single valid JSON object.
                Languages: de (German), en (English), fr (French), es (Spanish), tr (Turkish), \
                pl (Polish), ru (Russian), ee (Estonian), lv (Latvian), lt (Lithuanian).
                The JSON must use the text index as the key (as a string: "0", "1", ...) and an \
                object with language codes as the value.
                Return ONLY the JSON object, no markdown, no explanations, no extra text.
                Example for 2 texts:
                {"0":{"de":"...","en":"...","fr":"...","es":"...","tr":"...","pl":"...","ru":"...","ee":"...","lv":"...","lt":"..."},\
                "1":{"de":"...","en":"...","fr":"...","es":"...","tr":"...","pl":"...","ru":"...","ee":"...","lv":"...","lt":"..."}}
                """;

        // Estimate output tokens: ~400 tokens per text × languages, capped at 8 000
        int maxTokens = Math.min(500 + 400 * texts.size(), 8_000);

        try {
            String raw = llmRestClient.chat(systemPrompt, textsListing.toString(), 0.3, maxTokens);
            if (raw != null && !raw.isBlank()) {
                raw = extractJsonObject(raw);
                Map<String, Map<String, String>> parsed = objectMapper.readValue(raw, new TypeReference<>() {
                });
                Map<Integer, Map<String, String>> indexed = new LinkedHashMap<>();
                parsed.forEach((key, value) -> {
                    try {
                        indexed.put(Integer.parseInt(key), value);
                    } catch (NumberFormatException e) {
                        log.warn("[TranslationCache] Unexpected key in batch LLM response: {}", key);
                    }
                });
                return indexed;
            }
        } catch (Exception e) {
            log.warn("[TranslationCache] Batch LLM call failed: {}", e.getMessage());
        }

        return Map.of();
    }

    private List<Translation> buildTranslationList(String fallback, Map<String, String> translationMap) {
        List<Translation> list = new ArrayList<>();
        for (Language lang : getAvailableLanguages()) {
            String code = lang.name().toLowerCase();
            String translated = translationMap.getOrDefault(code, fallback);
            list.add(new Translation(code, (translated == null || translated.isBlank()) ? fallback : translated));
        }
        return list;
    }

    private static String extractJsonObject(String raw) {
        int start = raw.indexOf('{');
        int end = raw.lastIndexOf('}');
        if (start >= 0 && end > start) {
            return raw.substring(start, end + 1);
        }
        return raw;
    }

    private static Map<String, String> toTranslationMap(List<Translation> translations) {
        Map<String, String> map = new LinkedHashMap<>();
        for (Translation t : translations) {
            map.put(t.language(), t.text());
        }
        return map;
    }

    private static List<Translation> toTranslationList(Map<String, String> map) {
        List<Translation> list = new ArrayList<>();
        if (map != null) {
            map.forEach((lang, text) -> list.add(new Translation(lang, text)));
        }
        return list;
    }
}
