package com.gcsc.connection.gateway.service;

import com.gcsc.connection.apikey.entity.ApiKeyStatus;
import com.gcsc.connection.apikey.entity.SnpApiKey;
import com.gcsc.connection.apikey.repository.SnpApiKeyRepository;
import com.gcsc.connection.apikey.repository.SnpApiPermissionRepository;
import com.gcsc.connection.common.exception.BusinessException;
import com.gcsc.connection.common.exception.ErrorCode;
import com.gcsc.connection.common.util.AesEncryptor;
import com.gcsc.connection.monitoring.entity.SnpApiRequestLog;
import com.gcsc.connection.monitoring.repository.SnpApiRequestLogRepository;
import com.gcsc.connection.monitoring.service.RequestLogService;
import com.gcsc.connection.service.entity.SnpService;
import com.gcsc.connection.service.entity.SnpServiceApi;
import com.gcsc.connection.service.repository.SnpServiceApiRepository;
import com.gcsc.connection.service.repository.SnpServiceRepository;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;

@Service
@Slf4j
@RequiredArgsConstructor
public class GatewayService {

    private static final int API_KEY_PREFIX_LENGTH = 8;
    private static final String AUTH_KEY_PARAM = "authKey";
    private static final Set<String> EXCLUDED_HEADERS = Set.of(
        "host", "connection", "content-length"
    );

    private final SnpApiKeyRepository snpApiKeyRepository;
    private final SnpApiPermissionRepository snpApiPermissionRepository;
    private final SnpServiceRepository snpServiceRepository;
    private final SnpServiceApiRepository snpServiceApiRepository;
    private final AesEncryptor aesEncryptor;
    private final WebClient webClient;
    private final RequestLogService requestLogService;
    private final SnpApiRequestLogRepository snpApiRequestLogRepository;

    /**
     * API Gateway 프록시 요청 처리
     */
    public ResponseEntity<byte[]> proxyRequest(String serviceCode, String remainingPath,
                                                HttpServletRequest request) {
        LocalDateTime requestedAt = LocalDateTime.now();
        long startTime = System.currentTimeMillis();
        SnpApiKey apiKey = null;
        SnpService service = null;
        String gatewayPath = "/gateway/" + serviceCode + remainingPath;
        String targetUrl = null;

        try {
            // 1. 서비스 조회 (모든 로그에 service_id 기록을 위해 최우선 수행)
            service = snpServiceRepository.findByServiceCode(serviceCode)
                .orElseThrow(() -> new BusinessException(ErrorCode.SERVICE_NOT_FOUND));

            if (!Boolean.TRUE.equals(service.getIsActive())) {
                throw new BusinessException(ErrorCode.GATEWAY_SERVICE_INACTIVE);
            }

            // 2. 대상 URL 조합 (실패 로그에도 사용)
            targetUrl = buildTargetUrl(service.getServiceUrl(), remainingPath, request);

            // 3. API Key 추출 (쿼리 파라미터 authKey)
            String rawKey = request.getParameter(AUTH_KEY_PARAM);
            if (rawKey == null || rawKey.isBlank()) {
                throw new BusinessException(ErrorCode.GATEWAY_API_KEY_MISSING);
            }

            // 4. API Key 검증 (prefix 매칭 후 복호화 비교)
            apiKey = findApiKeyByRawKey(rawKey);

            // 5. Key 상태/만료 검증
            validateApiKey(apiKey);

            // 6. 일일 요청량 제한 검증
            validateDailyLimit(apiKey);

            // 7. ServiceApi 조회 (경로 + 메서드 매칭, {변수} 패턴 지원)
            String apiPath = remainingPath.startsWith("/") ? remainingPath : "/" + remainingPath;
            SnpServiceApi serviceApi = matchServiceApi(service.getServiceId(), apiPath, request.getMethod());

            // 6. 권한 확인
            snpApiPermissionRepository
                .findByApiKeyApiKeyIdAndApiApiIdAndIsActiveTrue(apiKey.getApiKeyId(), serviceApi.getApiId())
                .orElseThrow(() -> new BusinessException(ErrorCode.GATEWAY_PERMISSION_DENIED));

            // 7. 프록시 요청 전송
            ResponseEntity<byte[]> response = forwardRequest(targetUrl, request);

            // 9. 마지막 사용 시간 갱신
            apiKey.updateLastUsedAt();
            snpApiKeyRepository.save(apiKey);

            // 10. 성공 로그 저장
            int responseTime = (int) (System.currentTimeMillis() - startTime);
            saveLog(request, service, apiKey, targetUrl, gatewayPath, "SUCCESS",
                response.getStatusCode().value(), responseTime,
                response.getBody() != null ? (long) response.getBody().length : 0L,
                null, requestedAt);

            return response;

        } catch (BusinessException e) {
            int responseTime = (int) (System.currentTimeMillis() - startTime);
            String logStatus = isDeniedError(e.getErrorCode()) ? "DENIED" : "FAIL";
            saveLog(request, service, apiKey, targetUrl, gatewayPath, logStatus,
                e.getErrorCode().getStatus(), responseTime, 0L,
                e.getErrorCode().getMessage(), requestedAt);
            throw e;

        } catch (Exception e) {
            int responseTime = (int) (System.currentTimeMillis() - startTime);
            log.error("Gateway 프록시 요청 실패: {}", e.getMessage(), e);
            saveLog(request, service, apiKey, targetUrl, gatewayPath, "ERROR",
                502, responseTime, 0L, e.getMessage(), requestedAt);
            throw new BusinessException(ErrorCode.GATEWAY_PROXY_FAILED);
        }
    }

    /**
     * prefix 매칭 후 복호화하여 API Key 찾기
     */
    /**
     * ServiceApi 매칭 ({변수} 패턴 지원)
     * 등록된 /api/v1/tracks/haegu/{haeguNo} 가 /api/v1/tracks/haegu/101 과 매칭됨
     */
    private SnpServiceApi matchServiceApi(Long serviceId, String requestPath, String method) {
        // 1. 정확 매칭 시도
        var exact = snpServiceApiRepository
            .findByServiceServiceIdAndApiPathAndApiMethod(serviceId, requestPath, method);
        if (exact.isPresent()) {
            return exact.get();
        }

        // 2. {변수} 패턴 매칭
        List<SnpServiceApi> candidates = snpServiceApiRepository.findByServiceServiceId(serviceId);
        for (SnpServiceApi api : candidates) {
            if (!api.getApiMethod().equalsIgnoreCase(method)) continue;
            if (!Boolean.TRUE.equals(api.getIsActive())) continue;

            String pattern = api.getApiPath();
            if (!pattern.contains("{")) continue;

            // {variable}을 [^/]+ 정규식으로 치환
            String regex = pattern.replaceAll("\\{[^}]+}", "[^/]+");
            if (requestPath.matches(regex)) {
                return api;
            }
        }

        throw new BusinessException(ErrorCode.GATEWAY_PERMISSION_DENIED);
    }

    private SnpApiKey findApiKeyByRawKey(String rawKey) {
        if (rawKey.length() < API_KEY_PREFIX_LENGTH) {
            throw new BusinessException(ErrorCode.GATEWAY_API_KEY_INVALID);
        }

        String prefix = rawKey.substring(0, API_KEY_PREFIX_LENGTH);
        List<SnpApiKey> candidates = snpApiKeyRepository.findByApiKeyPrefix(prefix);

        for (SnpApiKey candidate : candidates) {
            try {
                String decryptedKey = aesEncryptor.decrypt(candidate.getApiKey());
                if (rawKey.equals(decryptedKey)) {
                    return candidate;
                }
            } catch (Exception e) {
                log.debug("API Key 복호화 비교 실패 (apiKeyId={})", candidate.getApiKeyId());
            }
        }

        throw new BusinessException(ErrorCode.GATEWAY_API_KEY_INVALID);
    }

    /**
     * API Key 상태 및 만료 검증
     */
    private void validateApiKey(SnpApiKey apiKey) {
        if (apiKey.getStatus() != ApiKeyStatus.ACTIVE) {
            throw new BusinessException(ErrorCode.GATEWAY_API_KEY_INVALID);
        }

        if (apiKey.getExpiresAt() != null && apiKey.getExpiresAt().isBefore(LocalDateTime.now())) {
            throw new BusinessException(ErrorCode.GATEWAY_API_KEY_EXPIRED);
        }
    }

    private static final Set<ErrorCode> DENIED_ERROR_CODES = Set.of(
        ErrorCode.GATEWAY_API_KEY_MISSING,
        ErrorCode.GATEWAY_API_KEY_INVALID,
        ErrorCode.GATEWAY_API_KEY_EXPIRED,
        ErrorCode.GATEWAY_PERMISSION_DENIED,
        ErrorCode.GATEWAY_DAILY_LIMIT_EXCEEDED
    );

    private boolean isDeniedError(ErrorCode errorCode) {
        return DENIED_ERROR_CODES.contains(errorCode);
    }

    /**
     * 일일 요청량 제한 검증
     */
    private void validateDailyLimit(SnpApiKey apiKey) {
        Long limit = apiKey.getDailyRequestLimit();
        if (limit == null) {
            return;
        }

        LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
        long todayCount = snpApiRequestLogRepository
            .countByApiKeyApiKeyIdAndRequestedAtGreaterThanEqual(apiKey.getApiKeyId(), startOfDay);

        if (todayCount >= limit) {
            throw new BusinessException(ErrorCode.GATEWAY_DAILY_LIMIT_EXCEEDED);
        }
    }

    /**
     * 대상 URL 구성
     */
    private String buildTargetUrl(String serviceUrl, String remainingPath, HttpServletRequest request) {
        StringBuilder url = new StringBuilder(serviceUrl);
        if (!remainingPath.isEmpty() && !"/".equals(remainingPath)) {
            if (!serviceUrl.endsWith("/") && !remainingPath.startsWith("/")) {
                url.append("/");
            }
            url.append(remainingPath);
        }

        String queryString = request.getQueryString();
        if (queryString != null && !queryString.isEmpty()) {
            // authKey 파라미터는 프록시 대상에 전달하지 않음
            String filtered = java.util.Arrays.stream(queryString.split("&"))
                .filter(p -> !p.startsWith(AUTH_KEY_PARAM + "="))
                .collect(java.util.stream.Collectors.joining("&"));
            if (!filtered.isEmpty()) {
                url.append("?").append(filtered);
            }
        }

        return url.toString();
    }

    /**
     * WebClient를 통한 프록시 요청 전송
     */
    private ResponseEntity<byte[]> forwardRequest(String targetUrl, HttpServletRequest request) {
        HttpMethod method = HttpMethod.valueOf(request.getMethod());

        WebClient.RequestBodySpec requestSpec = webClient.method(method)
            .uri(targetUrl)
            .headers(headers -> copyHeaders(request, headers));

        WebClient.RequestHeadersSpec<?> headersSpec;
        if (hasRequestBody(method)) {
            headersSpec = requestSpec.bodyValue(readRequestBody(request));
        } else {
            headersSpec = requestSpec;
        }

        try {
            return headersSpec.retrieve()
                .toEntity(byte[].class)
                .block();
        } catch (WebClientResponseException e) {
            // 대상 서버에서 에러 응답이 와도 그대로 전달
            return ResponseEntity
                .status(e.getStatusCode())
                .headers(e.getHeaders())
                .body(e.getResponseBodyAsByteArray());
        }
    }

    /**
     * 요청 헤더 복사 (제외 대상 헤더 필터링)
     */
    private void copyHeaders(HttpServletRequest request, HttpHeaders headers) {
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            if (!EXCLUDED_HEADERS.contains(headerName.toLowerCase())) {
                headers.set(headerName, request.getHeader(headerName));
            }
        }
    }

    /**
     * POST/PUT/PATCH 메서드 여부 확인
     */
    private boolean hasRequestBody(HttpMethod method) {
        return HttpMethod.POST.equals(method)
            || HttpMethod.PUT.equals(method)
            || HttpMethod.PATCH.equals(method);
    }

    /**
     * 요청 본문 읽기
     */
    private byte[] readRequestBody(HttpServletRequest request) {
        try {
            return request.getInputStream().readAllBytes();
        } catch (Exception e) {
            log.warn("요청 본문 읽기 실패", e);
            return new byte[0];
        }
    }

    /**
     * 비동기 요청 로그 저장
     */
    private void saveLog(HttpServletRequest request, SnpService service, SnpApiKey apiKey,
                         String targetUrl, String gatewayPath, String status, Integer responseStatus,
                         Integer responseTime, Long responseSize, String errorMessage,
                         LocalDateTime requestedAt) {
        try {
            SnpApiRequestLog logEntry = SnpApiRequestLog.builder()
                .requestUrl(gatewayPath)
                .requestMethod(request.getMethod())
                .requestStatus(status)
                .requestIp(getClientIp(request))
                .requestHeaders(extractHeaders(request))
                .requestParams(request.getQueryString())
                .service(service)
                .user(apiKey != null ? apiKey.getUser() : null)
                .apiKey(apiKey)
                .tenant(apiKey != null && apiKey.getUser() != null ? apiKey.getUser().getTenant() : null)
                .responseStatus(responseStatus)
                .responseTime(responseTime)
                .responseSize(responseSize)
                .errorMessage(errorMessage)
                .requestedAt(requestedAt)
                .build();

            requestLogService.saveLogAsync(logEntry);
        } catch (Exception e) {
            log.error("로그 엔트리 생성 실패", e);
        }
    }

    /**
     * 클라이언트 IP 추출
     */
    private String getClientIp(HttpServletRequest request) {
        String xForwardedFor = request.getHeader("X-Forwarded-For");
        if (xForwardedFor != null && !xForwardedFor.isBlank()) {
            return xForwardedFor.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }

    /**
     * 요청 헤더를 문자열로 변환
     */
    private String extractHeaders(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            sb.append(name).append(": ").append(request.getHeader(name)).append("\n");
        }
        return sb.toString();
    }
}
