package incheon.product.geoview3d.traffic.service.impl;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import incheon.product.common.geo3d.ExternalApiClient;
import incheon.product.common.geo3d.GeoView3DProperties;
import incheon.product.geoview3d.traffic.mapper.TrafficMapper;
import incheon.product.geoview3d.traffic.service.TrafficService;
import incheon.product.geoview3d.traffic.vo.TrafficRowVO;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;

import javax.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 교통 정보 서비스 구현체.
 * ITS 실시간 교통 정보를 수집/캐싱하고 조회 기능을 제공한다.
 *
 * <ul>
 *   <li>교통 정보 조회 시 최신 수집 시각이 5분 이상 경과하면 비동기 갱신</li>
 *   <li>ConcurrentHashMap 기반 in-flight 중복 요청 방지</li>
 *   <li>ITS API 응답을 파싱하여 스냅샷/최신 테이블에 일괄 upsert</li>
 * </ul>
 */
@Slf4j
@Service("productTrafficService")
public class TrafficServiceImpl extends EgovAbstractServiceImpl implements TrafficService {

    private static final long STALE_THRESHOLD_MS = 5 * 60 * 1000L;
    private static final String INFLIGHT_KEY = "traffic";
    private static final String LOGIN_USER_ID = "SYSTEM";

    private final ConcurrentHashMap<String, Boolean> inFlight = new ConcurrentHashMap<>();
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Lazy
    @Resource(name = "productTrafficService")
    private TrafficService self;

    @Resource(name = "productTrafficMapper")
    private TrafficMapper trafficMapper;

    @Resource(name = "geoView3DProperties")
    private GeoView3DProperties properties;

    @Resource(name = "productExternalApiClient")
    private ExternalApiClient externalApiClient;

    @Resource
    private TransactionTemplate transactionTemplate;

    @Override
    public List<Map<String, Object>> getTrafficInfo(String roadType, double minX, double minY, double maxX, double maxY) {
        Timestamp latestTs = trafficMapper.findLatestCreatedTs();

        if (latestTs == null || isStale(latestTs)) {
            self.updateTrafficInfoAsync();
        }

        return trafficMapper.selectLinkGradesInBbox(roadType, minX, minY, maxX, maxY);
    }

    @Override
    public Map<String, Object> getRoadNames(String roadType, String searchName, int page, int size) {
        int offset = (page - 1) * size;
        List<Map<String, Object>> items = trafficMapper.selectRoadNames(roadType, searchName, offset, size);
        int totalCount = trafficMapper.selectRoadNameCount(roadType, searchName);

        Map<String, Object> result = new HashMap<>();
        result.put("items", items);
        result.put("totalCount", totalCount);
        return result;
    }

    @Override
    public List<Map<String, Object>> getLinks(String roadName) {
        return trafficMapper.selectLinksByRoadName(roadName);
    }

    @Async("productTrfExecutor")
    @Override
    public void updateTrafficInfoAsync() {
        if (inFlight.putIfAbsent(INFLIGHT_KEY, Boolean.TRUE) != null) {
            log.debug("교통 정보 수집이 이미 진행 중입니다.");
            return;
        }

        try {
            GeoView3DProperties.TrafficApi apiConfig = properties.getTrafficApi();
            String url = buildTrafficApiUrl(apiConfig);

            String response = externalApiClient.getForIts(url, String.class);

            if (response == null || response.isEmpty()) {
                log.warn("ITS 교통 API 응답이 비어있습니다.");
                return;
            }

            List<TrafficRowVO> rows = parseTrafficResponse(response);

            if (!rows.isEmpty()) {
                transactionTemplate.executeWithoutResult(status -> {
                    trafficMapper.upsertSnapshotBatch(rows, LOGIN_USER_ID);
                    trafficMapper.upsertLatestBatch(rows, LOGIN_USER_ID);
                    trafficMapper.deleteOldSnapshots();
                });
                log.info("교통 정보 수집 완료: {}건", rows.size());
            }
        } catch (Exception e) {
            log.error("교통 정보 수집 중 오류 발생", e);
        } finally {
            inFlight.remove(INFLIGHT_KEY);
        }
    }

    private boolean isStale(Timestamp ts) {
        return System.currentTimeMillis() - ts.getTime() > STALE_THRESHOLD_MS;
    }

    private String buildTrafficApiUrl(GeoView3DProperties.TrafficApi apiConfig) {
        return apiConfig.getBaseUrl() + apiConfig.getTrafficPath()
                + "?apiKey=" + apiConfig.getApiKey()
                + "&type=all&getType=json";
    }

    private List<TrafficRowVO> parseTrafficResponse(String response) {
        List<TrafficRowVO> rows = new ArrayList<>();
        if (response == null || response.isEmpty()) {
            log.warn("교통 API 응답이 비어있습니다.");
            return rows;
        }
        try {
            JsonNode root = objectMapper.readTree(response);
            JsonNode body = root.path("body");
            JsonNode items = body.isArray() ? body : body.path("items");

            if (items.isArray()) {
                Timestamp now = new Timestamp(System.currentTimeMillis());
                for (JsonNode item : items) {
                    TrafficRowVO row = new TrafficRowVO();
                    row.setRoadTypeCode(item.path("roadType").asText(null));
                    row.setLinkId(item.path("linkId").asText(null));
                    row.setStartNodeId(item.path("startNodeId").asText(null));
                    row.setEndNodeId(item.path("endNodeId").asText(null));
                    row.setRoadName(item.path("roadName").asText(null));
                    row.setSpeed(item.path("speed").isNull() ? null : item.path("speed").asDouble());
                    row.setTravelTime(item.path("travelTime").isNull() ? null : item.path("travelTime").asDouble());
                    row.setCreatedTs(now);
                    row.setRawJson(item.toString());
                    rows.add(row);
                }
            }
        } catch (Exception e) {
            log.error("교통 API 응답 파싱 오류", e);
        }
        return rows;
    }
}
