package incheon.sgp.ipd.transport.service.impl;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.context.annotation.Primary;
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import incheon.sgp.ipd.transport.mapper.TransportationMapper;
import incheon.sgp.ipd.transport.service.TransportationService;
import incheon.sgp.ipd.transport.vo.BusVO;
import incheon.sgp.ipd.transport.vo.SubwayShapeVO;
import incheon.sgp.ipd.transport.vo.SubwayVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.*;

@Slf4j
@Service
@Primary
@RequiredArgsConstructor
public class TransportationServiceImpl extends EgovAbstractServiceImpl implements TransportationService {

    private final TransportationMapper mapper;
    @Autowired private ObjectMapper objectMapper;
    private static final JSONParser JSON_PARSER = new JSONParser();
    
    @Value("${external.bus.arrival-service-key:}")
    private String busArrivalServiceKey;
    
    @Value("${external.bus.arrival_url:}")
    private String busArrivalServiceUrl;
    
    @Override
    public List<BusVO> getBusByRadius(double latitude, double longitude, double radius, int offset, int limit) {
        return new ArrayList<>(mapper.getBusListByRadius(latitude, longitude, radius, offset, limit));
    }

    @Override
    public List<SubwayVO> getSubwayByRadius(double latitude, double longitude, double radius, int offset, int limit) {
        return new ArrayList<>(mapper.getSubwayListByRadius(latitude, longitude, radius, offset, limit));
    }

    @Override
    public List<SubwayShapeVO> getSubwayStPolyGonRadius(
            double latitude, double longitude, double radius, int offset, int limit
    ) {
        List<SubwayShapeVO> result = new ArrayList<>(
                mapper.getSubwayStPolyGonRadius(latitude, longitude, radius, offset, limit)
        );
        return mapGeomToNode(result);
    }

    @Override
    public List<SubwayShapeVO> getSubwayLine(
            double latitude, double longitude, double radius, int offset, int limit
    ) {
        List<SubwayShapeVO> result = new ArrayList<>(
                mapper.getSubwayLine(latitude, longitude, radius, offset, limit)
        );
        return mapGeomToNode(result);
    }

    private List<SubwayShapeVO> mapGeomToNode(List<SubwayShapeVO> list) {
        for (SubwayShapeVO vo : list) {
            if (vo.getGeom() != null) {
                vo.setGeomNode(parseToJsonNode(vo.getGeom()));
                vo.setGeom(null);
            }
        }
        return list;
    }

    private JsonNode parseToJsonNode(String geom) {
        try {
            return objectMapper.readTree(geom);
        } catch (JsonProcessingException e) {
            return null;
        }
    }

    @Override
    public List<SubwayVO> getSubwaySt(double latitude, double longitude, double radius, int offset, int limit) {
    	return new ArrayList<>(mapper.getSubwaySt(latitude, longitude, radius, offset, limit));
    }

    @Override
    public List<SubwayVO> getSubwayExit(double latitude, double longitude, double radius, int offset, int limit) {
    	return new ArrayList<>(mapper.getSubwayExit(latitude, longitude, radius, offset, limit));
    }

    private static final long BUS_CACHE_TTL_MS = getLong("BUS_CACHE_TTL_MS", 20_000L);
    
    private static long getLong(String key, long def) {
        String v = System.getProperty(key);
        if (!StringUtils.hasText(v)) v = System.getenv(key);
        return StringUtils.hasText(v) ? Long.parseLong(v) : def;
    }

    private final ConcurrentHashMap<String, CacheEntry> busArrivalCache = new ConcurrentHashMap<>();
    
    private static final class CacheEntry {
        final long ts; 
        final List<Map<String, Object>> data;
        CacheEntry(long ts, List<Map<String, Object>> data){ 
            this.ts = ts; 
            this.data = data; 
        }
    }

    private final ConcurrentHashMap<String, String> routeNameCache = new ConcurrentHashMap<>();
    @Autowired private RestTemplate restTemplate;

    private String resolveBusServiceKey() {
        if (StringUtils.hasText(busArrivalServiceKey)) {
            return busArrivalServiceKey.trim();
        }
        String k = System.getProperty("BUS_ARRIVAL_SERVICE_KEY");
        if (!StringUtils.hasText(k)) {
            k = System.getenv("BUS_ARRIVAL_SERVICE_KEY");
        }
        return k;
    }

    private boolean containsPercent(String value) {
        return value != null && value.contains("%");
    }

    @Override
    public List<Map<String, Object>> getBusArrival(String bstopId, Integer pageNo, Integer numOfRows) {
        Assert.hasText(bstopId, "bstopId required");

        final int p = (pageNo == null || pageNo < 1) ? 1 : pageNo;
        final int r = (numOfRows == null || numOfRows < 1) ? 10 : numOfRows;

        final String cacheKey = bstopId + "|" + p + "|" + r;
        if (BUS_CACHE_TTL_MS > 0) {
            CacheEntry ce = busArrivalCache.get(cacheKey);
            if (ce != null && (System.currentTimeMillis() - ce.ts) <= BUS_CACHE_TTL_MS) {
                return ce.data;
            }
        }
        
        String serviceKey = resolveBusServiceKey();
        boolean looksEncoded = containsPercent(serviceKey);

        URI uri = UriComponentsBuilder.fromHttpUrl(busArrivalServiceUrl)
                .queryParam("serviceKey", serviceKey)
                .queryParam("pageNo", p)
                .queryParam("numOfRows", r)
                .queryParam("bstopId", bstopId)
                .build(looksEncoded)
                .toUri();

        String xml;
        try {
            xml = requestXml(uri);
        } catch (HttpStatusCodeException ex) {
            log.error("[busArrival] request failed: {} body={}", ex.getStatusCode(), ex.getResponseBodyAsString());
            return List.of();
        } catch (Exception ex) {
            log.error("[busArrival] request failed: {}", ex.getMessage(), ex);
            return List.of();
        }
        if (!StringUtils.hasText(xml)) return List.of();

        List<Map<String, String>> rawItems = parseItemsFromXml(xml);
        if (rawItems.isEmpty()) {
            return List.of();
        }

        List<Map<String, Object>> out = new ArrayList<>(rawItems.size());
        for (Map<String, String> item : rawItems) {
            JsonNode node = objectMapper.valueToTree(item);
            Map<String, Object> row = extractItemFlexible(node, mapper);
            if (!row.isEmpty()) out.add(row);
        }

        if (BUS_CACHE_TTL_MS > 0) {
            busArrivalCache.put(cacheKey, new CacheEntry(System.currentTimeMillis(), out));
        }
        return out;
    }

    private String requestXml(URI uri) {
        HttpHeaders h = new HttpHeaders();
        h.add(HttpHeaders.ACCEPT, "*/*");
        RequestEntity<Void> req = new RequestEntity<>(h, HttpMethod.GET, uri);
        ResponseEntity<String> resp = restTemplate.exchange(req, String.class);
        if (resp.getStatusCode() == HttpStatus.OK) return resp.getBody();
        throw new HttpStatusCodeException(resp.getStatusCode(), "bus api not ok") {};
    }

    /**
     * XML을 간단하게 파싱 (보안 자동 처리)
     */
    /**
     * XML을 간단하게 파싱 (보안 자동 처리)
     */
    private List<Map<String, String>> parseItemsFromXml(String xml) {
        try {
            // XML -> JSON 문자열로 변환
            String jsonStr = xmlToJson(xml);
            
            // JSON 파싱
            JSONObject json = (JSONObject) JSON_PARSER.parse(jsonStr);
            
            // ServiceResult -> msgBody -> itemList 경로로 탐색
            JSONObject serviceResult = (JSONObject) json.get("ServiceResult");
            if (serviceResult == null) return List.of();
            
            JSONObject msgBody = (JSONObject) serviceResult.get("msgBody");
            if (msgBody == null) return List.of();
            
            Object itemObj = msgBody.get("itemList");
            if (itemObj == null) {
                // itemList가 없으면 item도 시도
                itemObj = msgBody.get("item");
            }
            if (itemObj == null) return List.of();
            
            List<Map<String, String>> items = new ArrayList<>();
            
            if (itemObj instanceof JSONArray) {
                JSONArray itemArray = (JSONArray) itemObj;
                for (Object obj : itemArray) {
                    if (obj instanceof JSONObject) {
                        items.add(jsonObjectToMap((JSONObject) obj));
                    }
                }
            } else if (itemObj instanceof JSONObject) {
                items.add(jsonObjectToMap((JSONObject) itemObj));
            }
            
            return items;
        } catch (Exception e) {
            log.error("[busArrival] XML parse error: {}", e.getMessage(), e);
            return List.of();
        }
    }

    /**
     * XML을 JSON 문자열로 변환
     */
    private String xmlToJson(String xml) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factory.newDocumentBuilder();
        Document doc = builder.parse(new java.io.ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
        
        Element root = doc.getDocumentElement();
        JSONObject json = new JSONObject();
        json.put(root.getNodeName(), elementToJson(root));
        
        return json.toJSONString();
    }

    /**
     * XML Element를 JSON으로 변환
     */
    @SuppressWarnings("unchecked")
    private Object elementToJson(Element element) {
        NodeList children = element.getChildNodes();
        
        // 텍스트만 있는 경우
        if (children.getLength() == 1 && children.item(0).getNodeType() == Node.TEXT_NODE) {
            return element.getTextContent().trim();
        }
        
        // 자식 요소가 있는 경우
        JSONObject obj = new JSONObject();
        Map<String, List<Object>> grouped = new LinkedHashMap<>();
        
        for (int i = 0; i < children.getLength(); i++) {
            Node node = children.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                Element child = (Element) node;
                String name = child.getNodeName();
                Object value = elementToJson(child);
                
                grouped.computeIfAbsent(name, k -> new ArrayList<>()).add(value);
            }
        }
        
        for (Map.Entry<String, List<Object>> entry : grouped.entrySet()) {
            if (entry.getValue().size() == 1) {
                obj.put(entry.getKey(), entry.getValue().get(0));
            } else {
                JSONArray arr = new JSONArray();
                arr.addAll(entry.getValue());
                obj.put(entry.getKey(), arr);
            }
        }
        
        return obj.isEmpty() ? element.getTextContent().trim() : obj;
    }

    @SuppressWarnings("unchecked")
    private Map<String, String> jsonObjectToMap(JSONObject jsonObject) {
        Map<String, String> map = new LinkedHashMap<>();
        for (Object key : jsonObject.keySet()) {
            Object value = jsonObject.get(key);
            if (value != null) {
                map.put(key.toString(), value.toString());
            }
        }
        return map;
    }

    private static Map<String, Object> extractItemFlexible(JsonNode it, TransportationMapper mapper) {
        Map<String, Object> m = new LinkedHashMap<>(16);

        String routeId = firstText(it, "ROUTEID", "routeId");

        put(m, "BSTOPID",             firstText(it, "BSTOPID", "bstopId", "stopId"));
        put(m, "ROUTEID",             routeId);
        put(m, "BUSID",               firstText(it, "BUSID", "busId"));
        put(m, "BUS_NUM_PLATE",       firstText(it, "BUS_NUM_PLATE", "busNumPlate", "plateNo", "plate"));
        put(m, "REST_STOP_COUNT",     firstInt(it, "REST_STOP_COUNT", "restStopCount", "remainingStop"));
        put(m, "ARRIVALESTIMATETIME", firstInt(it, "ARRIVALESTIMATETIME", "arrivalEstimateTime", "remainTime"));
        put(m, "LATEST_STOP_ID",      firstText(it, "LATEST_STOP_ID", "latestStopId"));
        put(m, "LATEST_STOP_NAME",    firstText(it, "LATEST_STOP_NAME", "latestStopName", "stopName"));
        put(m, "LOW_TP_CD",           firstInt(it, "LOW_TP_CD", "lowTpCd", "lowFloor"));
        put(m, "REMAIND_SEAT",        firstInt(it, "REMAIND_SEAT", "remaindSeat", "remainSeat"));
        put(m, "CONGESTION",          firstInt(it, "CONGESTION", "congestion"));
        put(m, "LASTBUSYN",           firstInt(it, "LASTBUSYN", "lastBusYn", "lastBus"));
        put(m, "DIRCD",               firstInt(it, "DIRCD", "dirCd", "direction"));

        if (routeId != null && !routeId.isEmpty() && mapper != null) {
            String routeName = mapper.getBusRouteName(routeId);
            if (routeName != null && !routeName.isEmpty()) {
                put(m, "ROUTE_NUMBER", routeName);
            }
        }

        return m;
    }
    
    private static String firstText(JsonNode n, String... keys){
        for (String k : keys){ 
            String v = text(n, k); 
            if (v != null && !v.isEmpty()) return v; 
        }
        return null;
    }
    
    private static Integer firstInt(JsonNode n, String... keys){
        for (String k : keys){ 
            Integer v = asInt(n, k); 
            if (v != null) return v; 
        }
        return null;
    }
    
    private static String text(JsonNode n, String f){
        return (n != null && n.has(f) && !n.get(f).isNull()) ? n.get(f).asText() : null;
    }
    
    private static Integer asInt(JsonNode n, String f){
        JsonNode v = (n != null) ? n.get(f) : null;
        if (v == null || v.isNull()) return null;
        return v.asInt();
    }
    
    private static void put(Map<String, Object> m, String k, String v){ 
        if (v != null) m.put(k, v); 
    }
    
    private static void put(Map<String, Object> m, String k, Integer v){ 
        if (v != null) m.put(k, v); 
    }

    @SuppressWarnings("unused")
    private String getBusNmByRouteId(String routeId) {
        if (!StringUtils.hasText(routeId)) return null;
        String cached = routeNameCache.get(routeId);
        if (cached != null) return cached;

        String ridTrim = routeId.trim();
        String name = null;
        
        if (StringUtils.hasText(name) && !routeId.equals(name)) {
            routeNameCache.put(routeId, name);
            return name;
        }
        return routeId;
    }

    @Override
    public String getBusRoute(String routeId, Integer pageNo, Integer numOfRows) {
        return mapper.getBusRouteName(routeId);
    }

    @Override
    public BusVO findBusByRadius(double latitude, double longitude, String name) {
        BusVO bus = mapper.findBusByRadius(latitude, longitude, name);
        return bus;
    }
}