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

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import incheon.com.cmm.exception.BusinessException;
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.CctvService;
import incheon.product.geoview3d.traffic.vo.CctvRowVO;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.http.HttpStatus;
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.net.HttpURLConnection;
import java.net.URL;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * CCTV 정보 서비스 구현체.
 * ITS CCTV 정보를 수집/캐싱하고 영상 URL 리다이렉트를 해석한다.
 *
 * <ul>
 *   <li>CCTV 목록 조회 시 최신 수집 시각이 5분 이상 경과하면 비동기 갱신</li>
 *   <li>ConcurrentHashMap 기반 in-flight 중복 요청 방지</li>
 *   <li>영상 URL HEAD 요청으로 리다이렉트 최종 URL 해석</li>
 * </ul>
 */
@Slf4j
@Service("productCctvService")
public class CctvServiceImpl extends EgovAbstractServiceImpl implements CctvService {

    private static final long STALE_THRESHOLD_MS = 5 * 60 * 1000L;
    private static final String INFLIGHT_KEY = "cctv";
    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 = "productCctvService")
    private CctvService 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>> getCctvList(Long gid, double minX, double minY, double maxX, double maxY) {
        Timestamp latestTs = trafficMapper.findLatestCreatedCctvTs();

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

        return trafficMapper.selectCctvList(gid, minX, minY, maxX, maxY);
    }

    @Override
    public String getCctvRedirectUrl(String url) {
        if (url == null || url.isEmpty()) {
            throw new BusinessException("URL이 비어있습니다.", HttpStatus.BAD_REQUEST);
        }

        // http/https 프로토콜만 허용
        if (!url.startsWith("http://") && !url.startsWith("https://")) {
            throw new BusinessException("허용되지 않는 프로토콜입니다. http 또는 https만 사용 가능합니다.", HttpStatus.BAD_REQUEST);
        }

        HttpURLConnection connection = null;
        try {
            GeoView3DProperties.TrafficApi apiConfig = properties.getTrafficApi();
            connection = (HttpURLConnection) new URL(url).openConnection();
            connection.setRequestMethod("HEAD");
            connection.setInstanceFollowRedirects(false);
            connection.setConnectTimeout(apiConfig.getConnectTimeoutMs());
            connection.setReadTimeout(apiConfig.getReadTimeoutMs());

            int responseCode = connection.getResponseCode();
            if (responseCode >= 300 && responseCode < 400) {
                String redirectUrl = connection.getHeaderField("Location");
                if (redirectUrl != null && !redirectUrl.isEmpty()) {
                    return redirectUrl;
                }
            }

            return url;
        } catch (Exception e) {
            log.warn("CCTV URL 리다이렉트 해석 실패: {}", url, e);
            return url;
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }

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

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

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

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

            List<CctvRowVO> rows = parseCctvResponse(response);

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

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

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

    private List<CctvRowVO> parseCctvResponse(String response) {
        List<CctvRowVO> rows = new ArrayList<>();
        if (response == null || response.isEmpty()) {
            log.warn("CCTV 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()) {
                for (JsonNode item : items) {
                    CctvRowVO row = new CctvRowVO();
                    row.setName(item.path("cctvname").asText(null));
                    row.setUrl(item.path("cctvurl").asText(null));
                    row.setCoordx(item.path("coordx").asText(null));
                    row.setCoordy(item.path("coordy").asText(null));
                    row.setBaseDate(item.path("baseDate").asText(null));
                    row.setRoadType(item.path("roadsectionId").asInt(0));
                    rows.add(row);
                }
            }
        } catch (Exception e) {
            log.error("CCTV API 응답 파싱 오류", e);
        }
        return rows;
    }
}
