package com.tasteby.service;

import com.tasteby.util.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Data pipeline: process pending videos end-to-end.
 * 1. Fetch transcript
 * 2. Extract restaurant info via LLM
 * 3. Geocode each restaurant
 * 4. Save to DB + generate vector embeddings
 */
@Service
public class PipelineService {

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

    private final YouTubeService youTubeService;
    private final ExtractorService extractorService;
    private final GeocodingService geocodingService;
    private final RestaurantService restaurantService;
    private final VideoService videoService;
    private final VectorService vectorService;
    private final CacheService cacheService;
    private final RestaurantVerifyService verifyService;
    private final VideoRelevanceService relevanceService;

    public PipelineService(YouTubeService youTubeService,
                            ExtractorService extractorService,
                            GeocodingService geocodingService,
                            RestaurantService restaurantService,
                            VideoService videoService,
                            VectorService vectorService,
                            CacheService cacheService,
                            RestaurantVerifyService verifyService,
                            VideoRelevanceService relevanceService) {
        this.youTubeService = youTubeService;
        this.extractorService = extractorService;
        this.geocodingService = geocodingService;
        this.restaurantService = restaurantService;
        this.videoService = videoService;
        this.vectorService = vectorService;
        this.cacheService = cacheService;
        this.verifyService = verifyService;
        this.relevanceService = relevanceService;
    }

    /**
     * Process a single pending video. Returns number of restaurants found.
     */
    public int processVideo(Map<String, Object> video) {
        String videoDbId = (String) video.get("id");
        String videoId = (String) video.get("video_id");
        String title = (String) video.get("title");

        log.info("Processing video: {} ({})", title, videoId);
        updateVideoStatus(videoDbId, "processing", null, null);

        try {
            // 1. Transcript
            var transcript = youTubeService.getTranscript(videoId, "auto");
            if (transcript == null || transcript.text() == null) {
                log.warn("No transcript for {}, marking done", videoId);
                updateVideoStatus(videoDbId, "done", null, null);
                return 0;
            }
            updateVideoStatus(videoDbId, "processing", transcript.text(), null);

            // 2. LLM extraction + geocode + save
            return processExtract(video, transcript.text(), null);

        } catch (Exception e) {
            log.error("Pipeline error for {}: {}", videoId, e.getMessage(), e);
            updateVideoStatus(videoDbId, "error", null, null);
            return 0;
        }
    }

    /**
     * Run LLM extraction + geocode + save on existing transcript.
     */
    @SuppressWarnings("unchecked")
    public int processExtract(Map<String, Object> video, String transcript, String customPrompt) {
        String videoDbId = (String) video.get("id");
        String title = (String) video.get("title");

        // #292 — 외부 가시성을 위해 진입 시 processing 전이 (이미 processing이면 no-op)
        updateVideoStatus(videoDbId, "processing", null, null);

        var result = extractorService.extractRestaurants(title, transcript, customPrompt);
        if (result.restaurants().isEmpty()) {
            updateVideoStatus(videoDbId, "done", null, result.rawResponse());
            return 0;
        }

        int count = 0;
        for (var restData : result.restaurants()) {
            String name = (String) restData.get("name");
            if (name == null) continue;

            // Geocode
            var geo = geocodingService.geocodeRestaurant(
                    name, (String) restData.get("address"));

            // Build upsert data
            var data = new HashMap<String, Object>();
            data.put("name", name);
            data.put("region", restData.get("region"));
            data.put("cuisine_type", restData.get("cuisine_type"));
            data.put("price_range", restData.get("price_range"));
            // #292 — geocode 실패(geo==null) 시 좌표/주소/place_id 등 기존 값 보존하기 위해
            // null을 명시적으로 put하지 않는다. upsert 측에서 누락 컬럼은 그대로 유지.
            if (geo != null) {
                data.put("address", geo.get("formatted_address"));
                data.put("latitude", geo.get("latitude"));
                data.put("longitude", geo.get("longitude"));
                data.put("google_place_id", geo.get("google_place_id"));
                data.put("phone", geo.get("phone"));
                data.put("website", geo.get("website"));
                data.put("business_status", geo.get("business_status"));
                data.put("rating", geo.get("rating"));
                data.put("rating_count", geo.get("rating_count"));
            } else {
                // geocode 실패한 첫 등록 케이스에서 최소한의 주소(LLM이 추출한 원시값)는 저장
                Object rawAddr = restData.get("address");
                if (rawAddr != null) data.put("address", rawAddr);
            }

            String restId = restaurantService.upsert(data);

            // Link video <-> restaurant
            var foods = restData.get("foods_mentioned");
            var evaluationRaw = restData.get("evaluation");
            var guests = restData.get("guests");

            // evaluation must be stored as valid JSON (DB has IS JSON check constraint)
            // Store as JSON string literal: "평가 내용" (valid JSON)
            String evaluationJson = null;
            if (evaluationRaw instanceof Map<?, ?>) {
                evaluationJson = JsonUtil.toJson(evaluationRaw);
            } else if (evaluationRaw instanceof String s && !s.isBlank()) {
                evaluationJson = JsonUtil.toJson(s);
            }

            String linkId = restaurantService.linkVideoRestaurant(
                    videoDbId, restId,
                    foods instanceof List<?> ? (List<String>) foods : null,
                    evaluationJson,
                    guests instanceof List<?> ? (List<String>) guests : null
            );

            // #356 — 신규 등록 직후 비동기 관련도 평가
            relevanceService.verifyAsync(linkId);

            // Vector embeddings
            var chunks = VectorService.buildChunks(name, restData, title);
            if (!chunks.isEmpty()) {
                try {
                    vectorService.saveRestaurantVectors(restId, chunks);
                } catch (Exception e) {
                    log.warn("Vector save failed for {}: {}", name, e.getMessage());
                }
            }

            count++;
            log.info("Saved restaurant: {} (geocoded={})", name, geo != null);

            // #322 — 등록 직후 비동기 LLM 검증
            verifyService.verifyAsync(restId);
        }

        updateVideoStatus(videoDbId, "done", null, result.rawResponse());
        log.info("Video {} done: {} restaurants", video.get("video_id"), count);
        return count;
    }

    /**
     * Process up to `limit` pending videos.
     */
    public int processPending(int limit) {
        var videos = videoService.findPendingVideos(limit);
        if (videos.isEmpty()) {
            log.info("No pending videos");
            return 0;
        }
        int total = 0;
        for (var v : videos) {
            total += processVideo(v);
        }
        if (total > 0) cacheService.flush();
        return total;
    }

    private void updateVideoStatus(String videoDbId, String status, String transcript, String llmRaw) {
        videoService.updateVideoFields(videoDbId, status, transcript, llmRaw);
    }
}
