package gc.mda.signal_batch.batch.reader;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import gc.mda.signal_batch.domain.vessel.model.AisTargetEntity;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.time.OffsetDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * AIS Target Caffeine 캐시 매니저
 *
 * key: MMSI (String) — 문자 혼합 MMSI 장비 지원
 * value: AisTargetEntity
 *
 * 동작:
 * - 1분 주기 API Reader → Writer에서 캐시 업데이트
 * - 5분 집계 Job에서 캐시 스냅샷 추출 → VesselData 변환
 * - 기존 데이터보다 최신(messageTimestamp 기준)인 경우에만 업데이트
 *
 * TTL (프로파일별):
 * - local: 5분, dev: 60분, prod/prod-mpr: 120분
 */
@Slf4j
@Component
public class AisTargetCacheManager {

    private Cache<String, AisTargetEntity> cache;

    /**
     * 트랙 누적 버퍼 — 1분 API 호출마다 위치를 append, 5분 집계 시 drain
     * AtomicReference swap으로 drain 시 lock-free 처리
     */
    private final AtomicReference<ConcurrentHashMap<String, List<AisTargetEntity>>> trackBufferRef =
            new AtomicReference<>(new ConcurrentHashMap<>());

    @Value("${app.cache.ais-target.ttl-minutes:120}")
    private long ttlMinutes;

    @Value("${app.cache.ais-target.max-size:300000}")
    private int maxSize;

    @PostConstruct
    public void init() {
        this.cache = Caffeine.newBuilder()
                .maximumSize(maxSize)
                .expireAfterWrite(ttlMinutes, TimeUnit.MINUTES)
                .recordStats()
                .removalListener((String key, AisTargetEntity value, RemovalCause cause) -> {
                    if (cause != RemovalCause.REPLACED) {
                        log.trace("캐시 제거 - MMSI: {}, 원인: {}", key, cause);
                    }
                })
                .build();

        log.info("AIS Target Caffeine 캐시 초기화 - TTL: {}분, 최대 크기: {}", ttlMinutes, maxSize);
    }

    // ==================== 단건 조회/업데이트 ====================

    public Optional<AisTargetEntity> get(String mmsi) {
        return Optional.ofNullable(cache.getIfPresent(mmsi));
    }

    public void put(AisTargetEntity entity) {
        if (entity == null || entity.getMmsi() == null) {
            return;
        }

        String mmsi = entity.getMmsi();
        AisTargetEntity existing = cache.getIfPresent(mmsi);

        if (existing == null || isNewer(entity, existing)) {
            cache.put(mmsi, entity);
        }
    }

    // ==================== 배치 조회/업데이트 ====================

    public Map<String, AisTargetEntity> getAll(List<String> mmsiList) {
        if (mmsiList == null || mmsiList.isEmpty()) {
            return Collections.emptyMap();
        }
        return cache.getAllPresent(mmsiList);
    }

    /**
     * 여러 데이터 일괄 저장/업데이트
     * 기존 데이터보다 최신인 경우에만 업데이트
     */
    public void putAll(List<AisTargetEntity> entities) {
        if (entities == null || entities.isEmpty()) {
            return;
        }

        int updated = 0;
        int skipped = 0;

        for (AisTargetEntity entity : entities) {
            if (entity == null || entity.getMmsi() == null) {
                continue;
            }

            AisTargetEntity existing = cache.getIfPresent(entity.getMmsi());

            if (existing == null || isNewer(entity, existing)) {
                cache.put(entity.getMmsi(), entity);
                updated++;
            } else {
                skipped++;
            }
        }

        log.debug("캐시 배치 업데이트 - 입력: {}, 업데이트: {}, 스킵: {}, 현재 크기: {}",
                entities.size(), updated, skipped, cache.estimatedSize());
    }

    // ==================== 캐시 스냅샷 (t_ais_position 동기화용) ====================

    /**
     * 캐시의 모든 데이터 조회 (AisPositionSyncStep에서 사용)
     */
    public Collection<AisTargetEntity> getAllValues() {
        return cache.asMap().values();
    }

    // ==================== 트랙 누적 버퍼 (5분 집계용) ====================

    /**
     * 1분 API 호출 결과를 트랙 버퍼에 누적
     * MMSI별로 위치 이력을 쌓아 5분 집계 시 LineStringM 생성에 사용
     */
    public void appendAllForTrack(List<AisTargetEntity> entities) {
        if (entities == null || entities.isEmpty()) {
            return;
        }

        ConcurrentHashMap<String, List<AisTargetEntity>> buffer = trackBufferRef.get();
        int appended = 0;

        for (AisTargetEntity entity : entities) {
            if (entity == null || entity.getMmsi() == null
                    || entity.getLat() == null || entity.getLon() == null) {
                continue;
            }
            buffer.computeIfAbsent(entity.getMmsi(),
                    k -> Collections.synchronizedList(new ArrayList<>())).add(entity);
            appended++;
        }

        log.debug("트랙 버퍼 누적: {} 건 (버퍼 내 선박 수: {})", appended, buffer.size());
    }

    /**
     * 트랙 버퍼를 drain하여 반환하고 새 버퍼로 교체 (5분 집계 Job에서 호출)
     * AtomicReference swap으로 1분 Writer와 lock-free 동시성 보장
     *
     * @return MMSI별 누적 위치 목록 (보통 MMSI당 ~5개 포인트)
     */
    public Map<String, List<AisTargetEntity>> drainTrackBuffer() {
        ConcurrentHashMap<String, List<AisTargetEntity>> drained =
                trackBufferRef.getAndSet(new ConcurrentHashMap<>());

        long totalPoints = drained.values().stream().mapToLong(List::size).sum();
        log.info("트랙 버퍼 drain: {} 선박, {} 포인트", drained.size(), totalPoints);

        return drained;
    }

    /**
     * 트랙 버퍼 현재 크기 (모니터링용)
     */
    public Map<String, Object> getTrackBufferStats() {
        ConcurrentHashMap<String, List<AisTargetEntity>> buffer = trackBufferRef.get();
        long totalPoints = buffer.values().stream().mapToLong(List::size).sum();

        Map<String, Object> stats = new LinkedHashMap<>();
        stats.put("vesselCount", buffer.size());
        stats.put("totalPoints", totalPoints);
        stats.put("avgPointsPerVessel", buffer.isEmpty() ? 0 : String.format("%.1f", (double) totalPoints / buffer.size()));
        return stats;
    }

    // ==================== 캐시 관리 ====================

    public void evict(String mmsi) {
        cache.invalidate(mmsi);
    }

    public void clear() {
        long size = cache.estimatedSize();
        cache.invalidateAll();
        log.info("캐시 전체 삭제 - {} 건", size);
    }

    public long size() {
        return cache.estimatedSize();
    }

    public void cleanup() {
        cache.cleanUp();
    }

    // ==================== 통계 ====================

    public Map<String, Object> getStats() {
        CacheStats stats = cache.stats();

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("estimatedSize", cache.estimatedSize());
        result.put("maxSize", maxSize);
        result.put("ttlMinutes", ttlMinutes);
        result.put("hitCount", stats.hitCount());
        result.put("missCount", stats.missCount());
        result.put("hitRate", String.format("%.2f%%", stats.hitRate() * 100));
        result.put("evictionCount", stats.evictionCount());
        result.put("utilizationPercent", String.format("%.2f%%", (cache.estimatedSize() * 100.0 / maxSize)));

        return result;
    }

    public CacheStats getCacheStats() {
        return cache.stats();
    }

    // ==================== Private ====================

    private boolean isNewer(AisTargetEntity newEntity, AisTargetEntity existing) {
        OffsetDateTime newTs = newEntity.getMessageTimestamp();
        OffsetDateTime existingTs = existing.getMessageTimestamp();

        if (newTs == null) return false;
        if (existingTs == null) return true;

        return newTs.isAfter(existingTs);
    }
}
