package com.kt.event.analytics.service;

import com.kt.event.analytics.dto.response.*;
import com.kt.event.analytics.entity.EventStats;
import com.kt.event.analytics.entity.TimelineData;
import com.kt.event.analytics.repository.EventStatsRepository;
import com.kt.event.analytics.repository.TimelineDataRepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * User Timeline Analytics Service
 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserTimelineAnalyticsService {

    private final EventStatsRepository eventStatsRepository;
    private final TimelineDataRepository timelineDataRepository;
    private final RedisTemplate<String, String> redisTemplate;
    private final ObjectMapper objectMapper;

    private static final String CACHE_KEY_PREFIX = "analytics:user:timeline:";
    private static final long CACHE_TTL = 1800;

    public UserTimelineAnalyticsResponse getUserTimelineAnalytics(String userId, String interval,
                                                                   List<String> metrics, boolean refresh) {
        log.info("사용자 타임라인 분석 조회 시작: userId={}, interval={}, refresh={}", userId, interval, refresh);

        String cacheKey = CACHE_KEY_PREFIX + userId + ":" + interval;

        if (!refresh) {
            String cachedData = redisTemplate.opsForValue().get(cacheKey);
            if (cachedData != null) {
                try {
                    return objectMapper.readValue(cachedData, UserTimelineAnalyticsResponse.class);
                } catch (JsonProcessingException e) {
                    log.warn("캐시 역직렬화 실패: {}", e.getMessage());
                }
            }
        }

        List<EventStats> allEvents = eventStatsRepository.findAllByUserId(userId);
        if (allEvents.isEmpty()) {
            return buildEmptyResponse(userId, interval);
        }

        List<String> eventIds = allEvents.stream().map(EventStats::getEventId).collect(Collectors.toList());
        List<TimelineData> allTimelineData = timelineDataRepository.findByEventIdInOrderByTimestampAsc(eventIds);

        UserTimelineAnalyticsResponse response = buildTimelineResponse(userId, allEvents, allTimelineData, interval);

        try {
            String jsonData = objectMapper.writeValueAsString(response);
            redisTemplate.opsForValue().set(cacheKey, jsonData, CACHE_TTL, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.warn("캐시 저장 실패: {}", e.getMessage());
        }

        return response;
    }

    private UserTimelineAnalyticsResponse buildEmptyResponse(String userId, String interval) {
        LocalDateTime now = LocalDateTime.now();
        return UserTimelineAnalyticsResponse.builder()
                .userId(userId)
                .period(PeriodInfo.builder()
                        .startDate(now)
                        .endDate(now)
                        .durationDays(0)
                        .build())
                .totalEvents(0)
                .interval(interval != null ? interval : "daily")
                .dataPoints(new ArrayList<>())
                .trend(TrendAnalysis.builder().overallTrend("stable").build())
                .peakTime(PeakTimeInfo.builder().build())
                .lastUpdatedAt(LocalDateTime.now())
                .dataSource("empty")
                .build();
    }

    private UserTimelineAnalyticsResponse buildTimelineResponse(String userId, List<EventStats> allEvents,
                                                                 List<TimelineData> allTimelineData, String interval) {
        Map<LocalDateTime, TimelineDataPoint> aggregatedData = new LinkedHashMap<>();

        for (TimelineData data : allTimelineData) {
            LocalDateTime key = normalizeTimestamp(data.getTimestamp(), interval);
            aggregatedData.computeIfAbsent(key, k -> TimelineDataPoint.builder()
                    .timestamp(k)
                    .participants(0)
                    .views(0)
                    .engagement(0)
                    .conversions(0)
                    .build());

            TimelineDataPoint point = aggregatedData.get(key);
            point.setParticipants(point.getParticipants() + data.getParticipants());
            point.setViews(point.getViews() + data.getViews());
            point.setEngagement(point.getEngagement() + data.getEngagement());
            point.setConversions(point.getConversions() + data.getConversions());
        }

        List<TimelineDataPoint> dataPoints = new ArrayList<>(aggregatedData.values());

        TrendAnalysis trend = analyzeTrend(dataPoints);
        PeakTimeInfo peakTime = findPeakTime(dataPoints);

        return UserTimelineAnalyticsResponse.builder()
                .userId(userId)
                .period(buildPeriodFromEvents(allEvents))
                .totalEvents(allEvents.size())
                .interval(interval != null ? interval : "daily")
                .dataPoints(dataPoints)
                .trend(trend)
                .peakTime(peakTime)
                .lastUpdatedAt(LocalDateTime.now())
                .dataSource("cached")
                .build();
    }

    private LocalDateTime normalizeTimestamp(LocalDateTime timestamp, String interval) {
        switch (interval != null ? interval.toLowerCase() : "daily") {
            case "hourly":
                return timestamp.truncatedTo(ChronoUnit.HOURS);
            case "weekly":
                return timestamp.truncatedTo(ChronoUnit.DAYS).minusDays(timestamp.getDayOfWeek().getValue() - 1);
            case "monthly":
                return timestamp.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS);
            case "daily":
            default:
                return timestamp.truncatedTo(ChronoUnit.DAYS);
        }
    }

    private TrendAnalysis analyzeTrend(List<TimelineDataPoint> dataPoints) {
        if (dataPoints.size() < 2) {
            return TrendAnalysis.builder().overallTrend("stable").build();
        }

        int firstHalf = dataPoints.subList(0, dataPoints.size() / 2).stream()
                .mapToInt(TimelineDataPoint::getParticipants).sum();
        int secondHalf = dataPoints.subList(dataPoints.size() / 2, dataPoints.size()).stream()
                .mapToInt(TimelineDataPoint::getParticipants).sum();

        double growthRate = firstHalf > 0 ? ((double) (secondHalf - firstHalf) / firstHalf) * 100 : 0.0;
        String trend = growthRate > 5 ? "increasing" : (growthRate < -5 ? "decreasing" : "stable");

        return TrendAnalysis.builder()
                .overallTrend(trend)
                .build();
    }

    private PeakTimeInfo findPeakTime(List<TimelineDataPoint> dataPoints) {
        if (dataPoints.isEmpty()) {
            return PeakTimeInfo.builder().build();
        }

        TimelineDataPoint peak = dataPoints.stream()
                .max(Comparator.comparingInt(TimelineDataPoint::getParticipants))
                .orElse(null);

        return peak != null ? PeakTimeInfo.builder()
                .timestamp(peak.getTimestamp())
                .metric("participants")
                .value(peak.getParticipants())
                .description(peak.getViews() + " views at peak time")
                .build() : PeakTimeInfo.builder().build();
    }

    /**
     * 전체 이벤트의 생성/수정 시간 기반으로 period 계산
     */
    private PeriodInfo buildPeriodFromEvents(List<EventStats> events) {
        LocalDateTime start = events.stream()
                .map(EventStats::getCreatedAt)
                .min(LocalDateTime::compareTo)
                .orElse(LocalDateTime.now());

        LocalDateTime end = events.stream()
                .map(EventStats::getUpdatedAt)
                .max(LocalDateTime::compareTo)
                .orElse(LocalDateTime.now());

        return PeriodInfo.builder()
                .startDate(start)
                .endDate(end)
                .durationDays((int) ChronoUnit.DAYS.between(start, end))
                .build();
    }
}
