package com.tasteby.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tasteby.domain.Restaurant;
import com.tasteby.mapper.SearchMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
public class SearchService {

    private static final Logger log = LoggerFactory.getLogger(SearchService.class);
    private static final ObjectMapper JSON = new ObjectMapper();
    private static final TypeReference<List<Restaurant>> LIST_TYPE = new TypeReference<>() {};

    private final SearchMapper searchMapper;
    private final RestaurantService restaurantService;
    private final VectorService vectorService;
    private final CacheService cache;

    @Value("${app.search.max-distance:0.57}")
    private double maxDistance;

    public SearchService(SearchMapper searchMapper,
                          RestaurantService restaurantService,
                          VectorService vectorService,
                          CacheService cache) {
        this.searchMapper = searchMapper;
        this.restaurantService = restaurantService;
        this.vectorService = vectorService;
        this.cache = cache;
    }

    public List<Restaurant> search(String q, String mode, int limit) {
        String key = cache.makeKey("search", "q=" + q, "m=" + mode, "l=" + limit);
        String cached = cache.getRaw(key);
        if (cached != null) {
            try {
                // #293 — ObjectMapper 재사용 (필드 static)
                return JSON.readValue(cached, LIST_TYPE);
            } catch (Exception ignored) {}
        }

        List<Restaurant> result;
        switch (mode) {
            case "semantic" -> result = semanticSearch(q, limit);
            case "hybrid" -> {
                var kw = keywordSearch(q, limit);
                var sem = semanticSearch(q, limit);
                // #293 — semantic 결과에도 channels 부착 (이전: keyword에만 부착되어 hybrid에서 sem 결과는 채널 누락)
                if (!sem.isEmpty()) attachChannels(sem);
                Set<String> seen = new HashSet<>();
                var merged = new ArrayList<Restaurant>();
                for (var r : kw) { if (seen.add(r.getId())) merged.add(r); }
                for (var r : sem) { if (seen.add(r.getId())) merged.add(r); }
                result = merged.size() > limit ? merged.subList(0, limit) : merged;
            }
            case "keyword" -> result = keywordSearch(q, limit);
            default -> {
                // #293 — 알 수 없는 mode는 silent fallback 대신 경고 로그
                log.warn("Unknown search mode '{}', falling back to keyword", mode);
                result = keywordSearch(q, limit);
            }
        }

        cache.set(key, result);
        return result;
    }

    private List<Restaurant> keywordSearch(String q, int limit) {
        // #293 — LIKE 와일드카드 escape: 사용자 입력의 %, _, \ 를 리터럴로 처리.
        // SQL에서는 ESCAPE '\\' 절을 사용 (SearchMapper.xml).
        String escaped = q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
        String pattern = "%" + escaped + "%";
        List<Restaurant> results = searchMapper.keywordSearch(pattern, limit);
        if (!results.isEmpty()) {
            attachChannels(results);
        }
        return results;
    }

    private List<Restaurant> semanticSearch(String q, int limit) {
        try {
            var similar = vectorService.searchSimilar(q, Math.max(30, limit * 3), maxDistance);
            if (similar.isEmpty()) return List.of();

            Set<String> seen = new LinkedHashSet<>();
            for (var s : similar) {
                seen.add((String) s.get("restaurant_id"));
            }

            List<Restaurant> results = new ArrayList<>();
            for (String rid : seen) {
                if (results.size() >= limit) break;
                var r = restaurantService.findById(rid);
                if (r != null && r.getLatitude() != null) {
                    results.add(r);
                }
            }
            return results;
        } catch (Exception e) {
            log.warn("Semantic search failed, falling back to keyword: {}", e.getMessage());
            return keywordSearch(q, limit);
        }
    }

    private void attachChannels(List<Restaurant> restaurants) {
        List<String> ids = restaurants.stream().map(Restaurant::getId).filter(Objects::nonNull).toList();
        if (ids.isEmpty()) return;

        var channelRows = searchMapper.findChannelsByRestaurantIds(ids);
        Map<String, List<String>> chMap = new HashMap<>();
        for (var row : channelRows) {
            String rid = (String) row.getOrDefault("restaurant_id", row.get("RESTAURANT_ID"));
            String ch = (String) row.getOrDefault("channel_name", row.get("CHANNEL_NAME"));
            if (rid != null && ch != null) {
                chMap.computeIfAbsent(rid, k -> new ArrayList<>()).add(ch);
            }
        }
        for (var r : restaurants) {
            r.setChannels(chMap.getOrDefault(r.getId(), List.of()));
        }
    }
}
