package com.tasteby.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tasteby.domain.Restaurant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * #322 LLM 검증으로 잘못된/프랜차이즈 식당 자동 숨김.
 * 설계서: docs/design/322-restaurant-llm-verify/README.md
 */
@Service
public class RestaurantVerifyService {

    private static final Logger log = LoggerFactory.getLogger(RestaurantVerifyService.class);

    private final RestaurantService restaurantService;
    private final OciGenAiService genAi;
    private final ObjectMapper jsonMapper = new ObjectMapper();

    // 백필 시 LLM rate-limit 보호용 sleep (ms)
    private static final long BACKFILL_SLEEP_MS = 200;

    public RestaurantVerifyService(RestaurantService restaurantService, OciGenAiService genAi) {
        this.restaurantService = restaurantService;
        this.genAi = genAi;
    }

    @Async
    public void verifyAsync(String restaurantId) {
        try {
            verify(restaurantId);
        } catch (Exception e) {
            log.warn("verifyAsync failed for {}: {}", restaurantId, e.getMessage());
        }
    }

    public void verify(String restaurantId) {
        Restaurant r = restaurantService.findById(restaurantId);
        if (r == null) return;
        VerifyResult result;
        try {
            String prompt = buildPrompt(r);
            String response = genAi.chat(prompt, 120);
            result = parseVerifyResponse(response);
        } catch (Exception e) {
            // 안전한 기본값: LLM 실패 시 공개 유지(=hidden=0). verified_at은 미설정으로 남겨 재시도 가능.
            log.warn("verify({}) LLM failed: {} — keeping visible", restaurantId, e.getMessage());
            return;
        }
        applyResult(restaurantId, result);
    }

    /**
     * 미검증(verified_at IS NULL) 식당을 배치로 검증. 운영 trigger 용.
     * 반환: 이번 호출에서 처리한 개수.
     */
    public int verifyAll(int batchSize) {
        int total = 0;
        List<Restaurant> batch;
        while (!(batch = restaurantService.findUnverified(batchSize)).isEmpty()) {
            for (Restaurant r : batch) {
                try {
                    verify(r.getId());
                } catch (Exception e) {
                    log.warn("verifyAll({}) failed: {}", r.getId(), e.getMessage());
                }
                total++;
                try { Thread.sleep(BACKFILL_SLEEP_MS); } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    return total;
                }
            }
            if (batch.size() < batchSize) break;
        }
        return total;
    }

    // ---- pure helpers (tested separately) ----

    String buildPrompt(Restaurant r) {
        String foods = r.getFoodsMentioned() == null || r.getFoodsMentioned().isEmpty()
                ? "(없음)" : String.join(", ", r.getFoodsMentioned());
        return "당신은 식당 데이터 큐레이터다. 다음 식당이 (1) 실제 운영 식당인지, (2) 흔한 프랜차이즈인지 판정하라.\n\n" +
               "식당명: " + safe(r.getName()) + "\n" +
               "주소: " + safe(r.getAddress()) + "\n" +
               "지역: " + safe(r.getRegion()) + "\n" +
               "음식 분류: " + safe(r.getCuisineType()) + "\n" +
               "언급된 음식: " + foods + "\n\n" +
               "응답 형식(JSON만, 다른 텍스트 없이):\n" +
               "{\"valid\": true|false, \"is_franchise\": true|false, \"reason\": \"20자 이내\"}\n\n" +
               "가이드:\n" +
               "- valid=false: 식당 이름이 사람 이름, 영상 제목 일부, 일반 명사(\"점심\", \"맛집\"), " +
               "영문 prefix(\"name:\", \"title:\") 등 분명히 식당이 아닌 경우.\n" +
               "- is_franchise=true: 스타벅스, 맥도날드, 버거킹, 김밥천국, 본죽 등 전국 50개 이상 매장의 흔한 체인.\n" +
               "- 판단이 모호하면 valid=true, is_franchise=false (보수적).";
    }

    VerifyResult parseVerifyResponse(String raw) {
        if (raw == null) return VerifyResult.safeDefault();
        String json = extractJson(raw);
        if (json == null) return VerifyResult.safeDefault();
        try {
            JsonNode node = jsonMapper.readTree(json);
            boolean valid = node.path("valid").asBoolean(true);
            boolean isFranchise = node.path("is_franchise").asBoolean(false);
            String reason = node.path("reason").asText("");
            if (reason.length() > 100) reason = reason.substring(0, 100);
            return new VerifyResult(valid, isFranchise, reason);
        } catch (Exception e) {
            return VerifyResult.safeDefault();
        }
    }

    private void applyResult(String id, VerifyResult r) {
        if (!r.valid()) {
            restaurantService.markHidden(id, truncate("not_restaurant: " + r.reason(), 120));
        } else if (r.isFranchise()) {
            restaurantService.markHidden(id, truncate("franchise: " + r.reason(), 120));
        } else {
            restaurantService.markVerifiedClean(id);
        }
    }

    private static final Pattern JSON_BLOCK = Pattern.compile("\\{[^{}]*\\}", Pattern.DOTALL);

    private static String extractJson(String raw) {
        // 우선 그대로 시도
        String trimmed = raw.trim();
        if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed;
        // 마크다운 코드블록 또는 다른 텍스트에 감싸진 경우 정규식 추출
        Matcher m = JSON_BLOCK.matcher(raw);
        return m.find() ? m.group() : null;
    }

    private static String safe(String s) { return s == null ? "(미상)" : s; }
    private static String truncate(String s, int max) { return s.length() <= max ? s : s.substring(0, max); }

    public record VerifyResult(boolean valid, boolean isFranchise, String reason) {
        public static VerifyResult safeDefault() { return new VerifyResult(true, false, "parse_failed"); }
    }
}
