package incheon.sgp.thm.web;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

import javax.net.ssl.SSLContext;

import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import incheon.sgp.thm.dto.SgpThmSubwayDto;
import incheon.sgp.thm.dto.SgpThmTilesetClippingDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import incheon.sgp.thm.service.SgpThmService;
import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/sgp/thm")
@RequiredArgsConstructor
public class SgpThmApiController {

	private static final Logger LOGGER = LoggerFactory.getLogger(SgpThmApiController.class);
	private static final int STREAM_CONNECT_TIMEOUT_MS = 5000;
	private static final int STREAM_READ_TIMEOUT_MS = 8000;
	private static final int MAX_REDIRECT_COUNT = 5;

	private final SgpThmService sgpThmService;

	private MediaType jsonUtf8() {
		return new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8);
	}

	/** (1) 주제도 목록 JSON 배열 */
	@GetMapping(value = "/list", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<String> getThemeListJson() {
		String json = sgpThmService.getThemeListJson();
		return ResponseEntity.ok().contentType(jsonUtf8()).body(json);
	}

	/** (2) 단일 주제도 (검색 파라미터 없으면 전체) */
	@GetMapping(value = "/{thmId}", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<String> getThemeJson(@PathVariable String thmId, @RequestParam(required = false) String q,
			@RequestParam(required = false) Integer limit) {
		String json = sgpThmService.getThemeJson(thmId, q, limit);
		return ResponseEntity.ok().contentType(jsonUtf8()).body(json);
	}

	/** (3) 호환: /{thmId}/search?q=&limit= → 내부적으로 동일 함수 호출 */
	@GetMapping(value = "/{thmId}/search", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<String> searchThemeNodes(@PathVariable String thmId, @RequestParam String q,
			@RequestParam(required = false) Integer limit) {
		String json = sgpThmService.getThemeJson(thmId, q, limit);
		return ResponseEntity.ok().contentType(jsonUtf8()).body(json);
	}

	/** (3-1) simId 기반 시뮬레이션 JSON 조회 */
	@GetMapping(value = "/simulation", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<String> getSimulationBySimId(@RequestParam("simId") String simId) {
		String json = sgpThmService.getSimulationJsonBySimId(simId);
		return ResponseEntity.ok().contentType(jsonUtf8()).body(json);
	}

    /** (3-2) clipId 기반 클리핑 조회 */
    @GetMapping(value = "/clipping", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<SgpThmTilesetClippingDto> getTilesetClippingById(
            @RequestParam("clipId") String clipId
	) {
		SgpThmTilesetClippingDto clipping = sgpThmService.getTilesetClippingById(clipId);
		return ResponseEntity.ok().contentType(jsonUtf8()).body(clipping);
	}
	
	/** (3-3) 주제도 칼럼 보유값 리스트 (검색 필터 드롭다운에 사용) */
	@GetMapping(value = "/search/filter")
	public List<String> getFilterList(
		@RequestParam("thmId") String thmId, @RequestParam("filter") String filter
	) {
		List<String> filterList = sgpThmService.getFilterList(thmId, filter);
		return filterList;
	}

	/** (3-4) 주제도 계층 필터 값 목록 조회 */
	@GetMapping(value = "/search/filter/child")
	public List<String> getFilterListByParent(
		@RequestParam("thmId") String thmId,
		@RequestParam("filterName1") String filterName1,
		@RequestParam("filterValue") String filterValue,
		@RequestParam("filterName2") String filterName2
	) {
		return sgpThmService.getFilterListByParent(thmId, filterName1, filterValue, filterName2);
	}
	
	/** (4) 교통상황 */
	@GetMapping(value = "/trf", produces = MediaType.APPLICATION_JSON_VALUE)
	public List<Map<String,Object>> latestTraffic(
	        @RequestParam int roadType,                  // 0=모두, 1=고속도로, 2=국도
	        @RequestParam(required = false) Double minX,
	        @RequestParam(required = false) Double minY,
	        @RequestParam(required = false) Double maxX,
	        @RequestParam(required = false) Double maxY
	) {
		Map<String, Object> bbox = new java.util.HashMap<>();
		if (minX != null)
			bbox.put("minX", minX);
		else
			minX = 126.1033;

		if (minY != null)
			bbox.put("minY", minY);
		else
			minY = 37.21217;

		if (maxX != null)
			bbox.put("maxX", maxX);
		else
			maxX = 126.79755;

		if (maxY != null)
			bbox.put("maxY", maxY);
		else
			maxY = 37.82877;

		//백그라운드 적재 트리거 (non-blocking)
		LOGGER.info("thmService.updateTrafficInfoBg");
		sgpThmService.updateTrafficInfoBg();
		LOGGER.info("thmService.getLatestTrafficInfo");
		return sgpThmService.getLatestTrafficInfo(roadType, (double) minX, (double) minY, (double) maxX, (double) maxY);
	}
	
	/**
     * 도로 이름 + 등급 목록 조회
     * 예)
     *  GET /roads/names            -> 전체
     *  GET /roads/names?roadType=1 -> 고속국도만
     *  GET /roads/names?roadType=2 -> 고속국도 이외
     */
    @GetMapping("/trf/roads")
    public Map<String, Object> getRoadNames(
    	@RequestParam(required = false) Integer roadType,
		@RequestParam(required = false) String searchName,
		@RequestParam(required = false) Integer page,
		@RequestParam(required = false) Integer size
	) {
		if (roadType == null)
			roadType = 1;

		if (page == null || page < 1)
			page = 1;

		if (size == null || size < 1)
			size = 10;

        return sgpThmService.getRoadNames(roadType, searchName, page, size);
    }

    /**
     * 특정 도로 이름으로 링크 목록 조회
     * 예)
     *  GET /links/by-road-name?roadName=경인고속도로
     */
    @GetMapping("/trf/links")
    public List<Map<String, Object>> getLinksByRoadName(
            @RequestParam("roadName") String roadName
	) {
		return sgpThmService.getLinksByRoadName(roadName);
	}
	
	/** (5) 교통 CCTV 목록 */
	@GetMapping(value = "/trf/cctv", produces = MediaType.APPLICATION_JSON_VALUE)
	public List<Map<String, Object>> trafficCctv(
		@RequestParam(required = false) Integer gid,
		@RequestParam(required = false) Double minX,
		@RequestParam(required = false) Double minY,
		@RequestParam(required = false) Double maxX,
		@RequestParam(required = false) Double maxY
	) {
		
		if (minX == null)
			minX = 126.1033;

		if (minY == null)
			minY = 37.21217;

		if (maxX == null)
			maxX = 126.79755;

		if (maxY == null)
			maxY = 37.82877;

		//백그라운드 적재 트리거 (non-blocking)
	    LOGGER.info("thmService.updateTrafficCctvBg");
	    sgpThmService.updateTrafficCctvBg();
	    LOGGER.info("thmService.getLatestTrafficCctv");
	    return sgpThmService.getCctvList(gid, (double) minX, (double) minY, (double) maxX, (double) maxY);
	}

	/** (5-1) CCTV 신규 테이블 목록 */
	@GetMapping(value = "/trf/cctv/new", produces = MediaType.APPLICATION_JSON_VALUE)
	public List<Map<String, Object>> trafficCctvNew(
		@RequestParam(required = false) Double minX,
		@RequestParam(required = false) Double minY,
		@RequestParam(required = false) Double maxX,
		@RequestParam(required = false) Double maxY
	) {
		if (minX == null)
			minX = 126.1033;

		if (minY == null)
			minY = 37.21217;

		if (maxX == null)
			maxX = 126.79755;

		if (maxY == null)
			maxY = 37.82877;

		return sgpThmService.getCctvNewList((double) minX, (double) minY, (double) maxX, (double) maxY);
	}

    /**
     * 특정 지하철 정보 조회
     * 예) GET /subway?subwayName=인천1호선
     */
    @GetMapping("/subway")
	public SgpThmSubwayDto getSubwayBySubwayName(@RequestParam("subwayName") String subwayName) {
		return sgpThmService.getSubwayBySubwayName(subwayName);
	}
	
	/**
	 * 인천시 구/군별 종량제봉투 가격 정보
	 */
	@GetMapping("/wastebag")
	public List<Map<String, Object>> getWasteBagPriceList(@RequestParam("sgg") String sggName) {
		return sgpThmService.getWasteBagPriceList(sggName);
	}
		
	@GetMapping("/trf/cctv/redirect")
	public ResponseEntity<Map<String, String>> getStreamingUrl(@RequestParam("url") String url)
		throws Exception {

			URI uri;
			try {
				uri = new URI(url);
			} catch (URISyntaxException e) {
				return ResponseEntity.badRequest().body(Map.of("error", "invalid url"));
			}

			// 최소한의 방어 (SSRF 방지용으로는 보통 도메인 allowlist까지 권장)
			String scheme = uri.getScheme();
			if (scheme == null || !(scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
				return ResponseEntity.badRequest().body(Map.of("error", "only http/https allowed"));
			}

		try {
			String finalUrl = resolveStreamingUrl(uri);
			return ResponseEntity.ok(Map.of("url", finalUrl));
		} catch (IllegalArgumentException e) {
			return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
		}
	}

	private String resolveStreamingUrl(URI uri) throws Exception {
		URI currentUri = uri;

		for (int i = 0; i < MAX_REDIRECT_COUNT; i++) {
			validateHttpUri(currentUri);

			RestTemplate restTemplate = createSecureRestTemplate(
					STREAM_CONNECT_TIMEOUT_MS,
					STREAM_READ_TIMEOUT_MS,
					currentUri.getHost());

			ResponseEntity<String> response = executeStreamingRequest(restTemplate, currentUri, HttpMethod.HEAD);
			if (response == null) {
				response = executeStreamingRequest(restTemplate, currentUri, HttpMethod.GET);
			}

			if (response == null) {
				return currentUri.toString();
			}

			if (!response.getStatusCode().is3xxRedirection()) {
				return currentUri.toString();
			}

			URI location = response.getHeaders().getLocation();
			if (location == null) {
				return currentUri.toString();
			}

			currentUri = currentUri.resolve(location);
		}

		return currentUri.toString();
	}

	private ResponseEntity<String> executeStreamingRequest(RestTemplate restTemplate, URI uri, HttpMethod method) {
		HttpHeaders headers = new HttpHeaders();
		headers.set(HttpHeaders.USER_AGENT, "Mozilla/5.0");

		try {
			return restTemplate.exchange(uri, method, new HttpEntity<>(headers), String.class);
		} catch (HttpStatusCodeException e) {
			return ResponseEntity.status(e.getStatusCode())
					.headers(e.getResponseHeaders() != null ? e.getResponseHeaders() : HttpHeaders.EMPTY)
					.body(e.getResponseBodyAsString());
		} catch (Exception e) {
			return null;
		}
	}

	private void validateHttpUri(URI uri) {
		String scheme = uri.getScheme();
		if (scheme == null || !(scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
			throw new IllegalArgumentException("only http/https allowed");
		}
		if (uri.getHost() == null || uri.getHost().isBlank()) {
			throw new IllegalArgumentException("invalid host");
		}
	}

	private RestTemplate createSecureRestTemplate(int connectTimeout, int readTimeout, String... allowedHosts)
			throws Exception {
		SSLContext sslContext = SSLContextBuilder.create()
				.loadTrustMaterial((chain, authType) -> true)
				.build();

		String[] enabledProtocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3" };

		SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
				sslContext,
				enabledProtocols,
				null,
				(hostname, session) -> {
					for (String allowedHost : allowedHosts) {
						if (allowedHost != null && hostname.equalsIgnoreCase(allowedHost.trim())) {
							return true;
						}
					}
					return false;
				});

		CloseableHttpClient httpClient = HttpClients.custom()
				.disableRedirectHandling()
				.setSSLSocketFactory(socketFactory)
				.build();

		HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
		factory.setConnectTimeout(connectTimeout);
		factory.setReadTimeout(readTimeout);

		return new RestTemplate(factory);
	}
}
