package com.tasteby.controller;

import com.tasteby.domain.VideoDetail;
import com.tasteby.domain.VideoSummary;
import com.tasteby.security.AuthUtil;
import com.tasteby.service.*;
import com.tasteby.util.JsonUtil;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

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

@RestController
@RequestMapping("/api/videos")
public class VideoController {

    private final VideoService videoService;
    private final CacheService cache;
    private final YouTubeService youTubeService;
    private final PipelineService pipelineService;
    private final ExtractorService extractorService;
    private final RestaurantService restaurantService;
    private final GeocodingService geocodingService;

    public VideoController(VideoService videoService, CacheService cache,
                           YouTubeService youTubeService, PipelineService pipelineService,
                           ExtractorService extractorService, RestaurantService restaurantService,
                           GeocodingService geocodingService) {
        this.videoService = videoService;
        this.cache = cache;
        this.youTubeService = youTubeService;
        this.pipelineService = pipelineService;
        this.extractorService = extractorService;
        this.restaurantService = restaurantService;
        this.geocodingService = geocodingService;
    }

    @GetMapping
    public List<VideoSummary> list(@RequestParam(required = false) String status) {
        return videoService.findAll(status);
    }

    @GetMapping("/{id}")
    public VideoDetail detail(@PathVariable String id) {
        var video = videoService.findDetail(id);
        if (video == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Video not found");
        return video;
    }

    @PutMapping("/{id}")
    public Map<String, Object> updateTitle(@PathVariable String id, @RequestBody Map<String, String> body) {
        AuthUtil.requireAdmin();
        String title = body.get("title");
        if (title == null || title.isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "title is required");
        }
        videoService.updateTitle(id, title);
        cache.flush();
        return Map.of("ok", true);
    }

    @PostMapping("/{id}/skip")
    public Map<String, Object> skip(@PathVariable String id) {
        AuthUtil.requireAdmin();
        videoService.updateStatus(id, "skip");
        cache.flush();
        return Map.of("ok", true);
    }

    @DeleteMapping("/{id}")
    public Map<String, Object> delete(@PathVariable String id) {
        AuthUtil.requireAdmin();
        videoService.delete(id);
        cache.flush();
        return Map.of("ok", true);
    }

    @DeleteMapping("/{videoId}/restaurants/{restaurantId}")
    public Map<String, Object> deleteVideoRestaurant(
            @PathVariable String videoId, @PathVariable String restaurantId) {
        AuthUtil.requireAdmin();
        videoService.deleteVideoRestaurant(videoId, restaurantId);
        cache.flush();
        return Map.of("ok", true);
    }

    @PostMapping("/{id}/fetch-transcript")
    public Map<String, Object> fetchTranscript(@PathVariable String id,
                                                @RequestParam(defaultValue = "auto") String mode) {
        AuthUtil.requireAdmin();
        var video = videoService.findDetail(id);
        if (video == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Video not found");

        var result = youTubeService.getTranscript(video.getVideoId(), mode);
        if (result == null || result.text() == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No transcript available");
        }

        videoService.updateTranscript(id, result.text());
        return Map.of("ok", true, "length", result.text().length(), "source", result.source());
    }

    /** 클라이언트(브라우저)에서 가져온 트랜스크립트를 저장 */
    @PostMapping("/{id}/upload-transcript")
    public Map<String, Object> uploadTranscript(@PathVariable String id,
                                                 @RequestBody Map<String, String> body) {
        AuthUtil.requireAdmin();
        var video = videoService.findDetail(id);
        if (video == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Video not found");

        String text = body.get("text");
        if (text == null || text.isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "text is required");
        }

        videoService.updateTranscript(id, text);
        String source = body.getOrDefault("source", "browser");
        return Map.of("ok", true, "length", text.length(), "source", source);
    }

    @GetMapping("/extract/prompt")
    public Map<String, Object> getExtractPrompt() {
        return Map.of("prompt", extractorService.getPrompt());
    }

    @PostMapping("/{id}/extract")
    public Map<String, Object> extract(@PathVariable String id,
                                        @RequestBody(required = false) Map<String, String> body) {
        AuthUtil.requireAdmin();
        var video = videoService.findDetail(id);
        if (video == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Video not found");
        if (video.getTranscriptText() == null || video.getTranscriptText().isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No transcript");
        }

        String customPrompt = body != null ? body.get("prompt") : null;
        var videoMap = Map.<String, Object>of("id", id, "video_id", video.getVideoId(), "title", video.getTitle());
        int count = pipelineService.processExtract(videoMap, video.getTranscriptText(), customPrompt);
        if (count > 0) cache.flush();
        return Map.of("ok", true, "restaurants_extracted", count);
    }

    @GetMapping("/bulk-extract/pending")
    public Map<String, Object> bulkExtractPending() {
        var videos = videoService.findVideosForBulkExtract();
        var summary = videos.stream().map(v -> Map.of("id", v.get("id"), "title", v.get("title"))).toList();
        return Map.of("count", videos.size(), "videos", summary);
    }

    @GetMapping("/bulk-transcript/pending")
    public Map<String, Object> bulkTranscriptPending() {
        var videos = videoService.findVideosWithoutTranscript();
        var summary = videos.stream().map(v -> Map.of("id", v.get("id"), "title", v.get("title"))).toList();
        return Map.of("count", videos.size(), "videos", summary);
    }

    @SuppressWarnings("unchecked")
    @PostMapping("/{videoId}/restaurants/manual")
    public Map<String, Object> addManualRestaurant(@PathVariable String videoId,
                                                    @RequestBody Map<String, Object> body) {
        AuthUtil.requireAdmin();
        String name = (String) body.get("name");
        if (name == null || name.isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "name is required");
        }

        // Geocode
        var geo = geocodingService.geocodeRestaurant(name, (String) body.get("address"));
        var data = new HashMap<String, Object>();
        data.put("name", name);
        data.put("address", geo != null ? geo.get("formatted_address") : body.get("address"));
        data.put("region", body.get("region"));
        data.put("latitude", geo != null ? geo.get("latitude") : null);
        data.put("longitude", geo != null ? geo.get("longitude") : null);
        data.put("cuisine_type", body.get("cuisine_type"));
        data.put("price_range", body.get("price_range"));
        data.put("google_place_id", geo != null ? geo.get("google_place_id") : null);
        data.put("phone", geo != null ? geo.get("phone") : null);
        data.put("website", geo != null ? geo.get("website") : null);
        data.put("business_status", geo != null ? geo.get("business_status") : null);
        data.put("rating", geo != null ? geo.get("rating") : null);
        data.put("rating_count", geo != null ? geo.get("rating_count") : null);

        String restId = restaurantService.upsert(data);

        // Parse foods and guests
        List<String> foods = null;
        Object foodsRaw = body.get("foods_mentioned");
        if (foodsRaw instanceof List<?>) {
            foods = ((List<?>) foodsRaw).stream().map(Object::toString).toList();
        } else if (foodsRaw instanceof String s && !s.isBlank()) {
            foods = List.of(s.split("\\s*,\\s*"));
        }

        List<String> guests = null;
        Object guestsRaw = body.get("guests");
        if (guestsRaw instanceof List<?>) {
            guests = ((List<?>) guestsRaw).stream().map(Object::toString).toList();
        } else if (guestsRaw instanceof String s && !s.isBlank()) {
            guests = List.of(s.split("\\s*,\\s*"));
        }

        String evaluation = body.get("evaluation") instanceof String s ? s : null;

        restaurantService.linkVideoRestaurant(videoId, restId, foods, evaluation, guests);
        cache.flush();
        return Map.of("ok", true, "restaurant_id", restId);
    }

    @SuppressWarnings("unchecked")
    @PutMapping("/{videoId}/restaurants/{restaurantId}")
    public Map<String, Object> updateVideoRestaurant(@PathVariable String videoId,
                                                      @PathVariable String restaurantId,
                                                      @RequestBody Map<String, Object> body) {
        AuthUtil.requireAdmin();

        // Update link fields (foods_mentioned, evaluation, guests)
        List<String> foods = null;
        Object foodsRaw = body.get("foods_mentioned");
        if (foodsRaw instanceof List<?>) {
            foods = ((List<?>) foodsRaw).stream().map(Object::toString).toList();
        } else if (foodsRaw instanceof String s && !s.isBlank()) {
            foods = List.of(s.split("\\s*,\\s*"));
        }

        List<String> guests = null;
        Object guestsRaw = body.get("guests");
        if (guestsRaw instanceof List<?>) {
            guests = ((List<?>) guestsRaw).stream().map(Object::toString).toList();
        } else if (guestsRaw instanceof String s && !s.isBlank()) {
            guests = List.of(s.split("\\s*,\\s*"));
        }

        // evaluation must be valid JSON for DB IS JSON constraint
        String evaluationJson = null;
        Object evalRaw = body.get("evaluation");
        if (evalRaw instanceof String s && !s.isBlank()) {
            evaluationJson = s.trim().startsWith("{") || s.trim().startsWith("\"") ? s : JsonUtil.toJson(s);
        } else if (evalRaw instanceof Map<?, ?>) {
            evaluationJson = JsonUtil.toJson(evalRaw);
        }
        String foodsJson = foods != null ? JsonUtil.toJson(foods) : null;
        String guestsJson = guests != null ? JsonUtil.toJson(guests) : null;
        videoService.updateVideoRestaurantFields(videoId, restaurantId, foodsJson, evaluationJson, guestsJson);

        // Update restaurant fields if provided
        var restFields = new HashMap<String, Object>();
        for (var key : List.of("name", "address", "region", "cuisine_type", "price_range")) {
            if (body.containsKey(key)) restFields.put(key, body.get(key));
        }
        if (!restFields.isEmpty()) {
            // Re-geocode if name or address changed
            var existing = restaurantService.findById(restaurantId);
            String newName = (String) restFields.get("name");
            String newAddr = (String) restFields.get("address");
            boolean nameChanged = newName != null && existing != null && !newName.equals(existing.getName());
            boolean addrChanged = newAddr != null && existing != null && !newAddr.equals(existing.getAddress());
            if (nameChanged || addrChanged) {
                String geoName = newName != null ? newName : existing.getName();
                String geoAddr = newAddr != null ? newAddr : existing.getAddress();
                var geo = geocodingService.geocodeRestaurant(geoName, geoAddr);
                if (geo != null) {
                    restFields.put("latitude", geo.get("latitude"));
                    restFields.put("longitude", geo.get("longitude"));
                    restFields.put("google_place_id", geo.get("google_place_id"));
                    if (geo.containsKey("formatted_address")) {
                        restFields.put("address", geo.get("formatted_address"));
                    }
                    if (geo.containsKey("rating")) restFields.put("rating", geo.get("rating"));
                    if (geo.containsKey("rating_count")) restFields.put("rating_count", geo.get("rating_count"));
                    if (geo.containsKey("phone")) restFields.put("phone", geo.get("phone"));
                    if (geo.containsKey("business_status")) restFields.put("business_status", geo.get("business_status"));
                    // Parse region from address
                    String addr = (String) geo.get("formatted_address");
                    if (addr != null) {
                        restFields.put("region", GeocodingService.parseRegionFromAddress(addr));
                    }
                }
            }
            restaurantService.update(restaurantId, restFields);
        }

        cache.flush();
        return Map.of("ok", true);
    }
}
