package tech.easyflow.ai.service.discovery;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import tech.easyflow.ai.entity.ModelProvider;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.io.IOException;
import java.io.InputStream;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URI;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Comparator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;

/**
 * 使用服务端已保存配置执行受控的远端模型目录请求。
 */
@Component
public class RemoteModelHttpClient {

    /** 日志记录器。 */
    private static final Logger log = LoggerFactory.getLogger(RemoteModelHttpClient.class);
    /** 最大响应体大小。 */
    private static final int MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
    /** 单次远端请求超时。 */
    private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(15);
    /** 允许访问本机或内网地址的服务商类型。 */
    private static final Set<String> PRIVATE_ENDPOINT_PROVIDER_TYPES = Set.of(
            "ollama", "self-hosted", "self_hosted", "selfhost");
    /** 始终禁止访问的元数据主机。 */
    private static final Set<String> BLOCKED_HOSTS = Set.of(
            "metadata.google.internal", "metadata.google.internal.", "100.100.100.200");

    /** HTTP 客户端。 */
    private final HttpClient httpClient;
    /** JSON 解析器。 */
    private final ObjectMapper objectMapper;

    /**
     * 创建受控远端模型 HTTP 客户端。
     *
     * @param objectMapper JSON 解析器
     */
    public RemoteModelHttpClient(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(8))
                .followRedirects(HttpClient.Redirect.NEVER)
                .version(HttpClient.Version.HTTP_1_1)
                .build();
    }

    /**
     * 请求并解析模型目录 JSON。
     *
     * @param provider 已保存的模型服务商
     * @param requestPath 静态适配器确定的请求路径
     * @param queryParameters 受控查询参数
     * @return JSON 根节点
     * @throws BusinessException URL、网络、状态码、响应大小或 JSON 格式不合法时抛出
     */
    public JsonNode getJson(ModelProvider provider,
                            String requestPath,
                            Map<String, String> queryParameters) {
        URI uri = buildUri(provider, requestPath, queryParameters);
        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(uri)
                .timeout(REQUEST_TIMEOUT)
                .header("Accept", "application/json")
                .header("Accept-Encoding", "gzip")
                .header("User-Agent", "EasyFlow-RemoteModelDiscovery/1.0")
                .GET();
        String apiKey = provider.getApiKey();
        if (apiKey != null && !apiKey.isBlank()) {
            String trimmedKey = apiKey.trim();
            if (trimmedKey.indexOf('\r') >= 0 || trimmedKey.indexOf('\n') >= 0) {
                throw new BusinessException(422, 42211, "API 密钥格式不正确");
            }
            requestBuilder.header("Authorization", "Bearer " + trimmedKey);
        }

        long startedAt = System.nanoTime();
        try {
            HttpResponse<InputStream> response = httpClient.send(
                    requestBuilder.build(), HttpResponse.BodyHandlers.ofInputStream());
            log.info("远端模型目录请求完成 providerId={}, providerType={}, host={}, status={}, elapsedMs={}",
                    provider.getId(), provider.getProviderType(), uri.getHost(), response.statusCode(),
                    Duration.ofNanos(System.nanoTime() - startedAt).toMillis());
            try (InputStream rawBody = response.body()) {
                validateStatus(response.statusCode());
                try (InputStream body = decodeResponseBody(response, rawBody)) {
                    byte[] payload = body.readNBytes(MAX_RESPONSE_BYTES + 1);
                    if (payload.length > MAX_RESPONSE_BYTES) {
                        throw new BusinessException(502, 50213, "远端模型列表响应过大");
                    }
                    JsonNode root;
                    try {
                        root = objectMapper.readTree(payload);
                    } catch (com.fasterxml.jackson.core.JsonProcessingException exception) {
                        throw new BusinessException(502, 50214,
                                "远端模型列表响应不是有效 JSON", exception);
                    }
                    if (root == null) {
                        throw new BusinessException(502, 50215, "远端模型列表响应为空");
                    }
                    return root;
                }
            }
        } catch (BusinessException exception) {
            throw exception;
        } catch (java.net.http.HttpTimeoutException exception) {
            log.error("远端模型目录请求超时 providerId={}, providerType={}, host={}",
                    provider.getId(), provider.getProviderType(), uri.getHost(), exception);
            throw new BusinessException(504, 50411, "获取模型列表超时，请检查 API 地址后重试", exception);
        } catch (InterruptedException exception) {
            Thread.currentThread().interrupt();
            log.error("远端模型目录请求被中断 providerId={}, providerType={}, host={}",
                    provider.getId(), provider.getProviderType(), uri.getHost(), exception);
            throw new BusinessException(503, 50311, "获取模型列表被中断，请稍后重试", exception);
        } catch (IOException | IllegalArgumentException exception) {
            log.error("远端模型目录请求失败 providerId={}, providerType={}, host={}",
                    provider.getId(), provider.getProviderType(), uri.getHost(), exception);
            throw new BusinessException(502, 50211, "无法获取模型列表，请检查 API 地址和密钥", exception);
        }
    }

    /**
     * 根据响应编码解压响应体，降低大型模型目录的网络传输开销。
     *
     * @param response HTTP 响应
     * @param rawBody 原始响应流
     * @return 可直接读取的响应流
     * @throws IOException gzip 响应无法解压时抛出
     */
    private InputStream decodeResponseBody(HttpResponse<InputStream> response,
                                           InputStream rawBody) throws IOException {
        String contentEncoding = response.headers()
                .firstValue("Content-Encoding")
                .orElse("")
                .trim();
        if ("gzip".equalsIgnoreCase(contentEncoding)) {
            return new GZIPInputStream(rawBody);
        }
        return rawBody;
    }

    /**
     * 合并服务商 Endpoint、静态路径和受控查询参数。
     *
     * @param provider 已保存的模型服务商
     * @param requestPath 静态请求路径
     * @param queryParameters 受控查询参数
     * @return 已完成安全校验的请求 URI
     * @throws BusinessException URL 或目标地址不安全时抛出
     */
    public URI buildUri(ModelProvider provider,
                        String requestPath,
                        Map<String, String> queryParameters) {
        if (provider == null || provider.getEndpoint() == null || provider.getEndpoint().isBlank()) {
            throw new BusinessException(422, 42212, "请先配置并保存 API 地址");
        }
        if (requestPath == null || requestPath.isBlank()
                || requestPath.contains("?") || requestPath.contains("#")) {
            throw new BusinessException(422, 42213, "当前服务暂不支持获取模型列表");
        }

        try {
            URI endpoint = URI.create(provider.getEndpoint().trim());
            validateEndpoint(provider, endpoint);
            String combinedPath = combinePaths(endpoint.getRawPath(), requestPath);
            String query = buildQuery(queryParameters);
            return new URI(endpoint.getScheme(), null, endpoint.getHost(), endpoint.getPort(),
                    combinedPath, query, null);
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            throw new BusinessException(422, 42214, "API 地址格式不正确", exception);
        }
    }

    /**
     * 校验 Endpoint 协议、主机与解析后的地址范围。
     *
     * @param provider 模型服务商
     * @param endpoint Endpoint URI
     */
    private void validateEndpoint(ModelProvider provider, URI endpoint) {
        String scheme = endpoint.getScheme() == null
                ? "" : endpoint.getScheme().toLowerCase(Locale.ROOT);
        if (!("http".equals(scheme) || "https".equals(scheme))
                || endpoint.getHost() == null
                || endpoint.getUserInfo() != null
                || endpoint.getQuery() != null
                || endpoint.getFragment() != null) {
            throw new BusinessException(422, 42214, "API 地址格式不正确");
        }
        String host = endpoint.getHost().toLowerCase(Locale.ROOT);
        if (BLOCKED_HOSTS.contains(host)) {
            throw new BusinessException(422, 42215, "API 地址指向受限网络目标");
        }

        boolean privateEndpointAllowed = PRIVATE_ENDPOINT_PROVIDER_TYPES.contains(
                normalize(provider.getProviderType()));
        try {
            for (InetAddress address : InetAddress.getAllByName(host)) {
                if (isAlwaysBlocked(address)
                        || (!privateEndpointAllowed && isPrivateOrLoopback(address))) {
                    throw new BusinessException(422, 42215, "API 地址指向受限网络目标");
                }
            }
        } catch (UnknownHostException exception) {
            throw new BusinessException(502, 50212, "API 地址无法解析", exception);
        }
    }

    /**
     * 判断地址是否在任何服务商下都禁止访问。
     *
     * @param address 已解析地址
     * @return 禁止访问返回 true
     */
    private boolean isAlwaysBlocked(InetAddress address) {
        return address.isAnyLocalAddress()
                || address.isLinkLocalAddress()
                || address.isMulticastAddress()
                || "100.100.100.200".equals(address.getHostAddress());
    }

    /**
     * 判断地址是否为内网或本机地址。
     *
     * @param address 已解析地址
     * @return 内网或本机地址返回 true
     */
    private boolean isPrivateOrLoopback(InetAddress address) {
        return address.isLoopbackAddress()
                || address.isSiteLocalAddress()
                || isUniqueLocalIpv6(address);
    }

    /**
     * 判断地址是否位于 IPv6 唯一本地地址段 fc00::/7。
     *
     * @param address 已解析地址
     * @return 位于 fc00::/7 返回 true
     */
    private boolean isUniqueLocalIpv6(InetAddress address) {
        if (!(address instanceof Inet6Address)) {
            return false;
        }
        return (address.getAddress()[0] & 0xFE) == 0xFC;
    }

    /**
     * 合并 Endpoint 路径与模型目录路径。
     *
     * @param basePath Endpoint 自带路径
     * @param requestPath 目录请求路径
     * @return 规范化请求路径
     */
    private String combinePaths(String basePath, String requestPath) {
        String normalizedBase = normalizePath(basePath);
        String normalizedRequest = normalizePath(requestPath);
        if ("/".equals(normalizedBase)) {
            return normalizedRequest;
        }
        if (normalizedRequest.equals(normalizedBase)
                || normalizedRequest.startsWith(normalizedBase + "/")) {
            return normalizedRequest;
        }
        return normalizePath(normalizedBase + "/" + normalizedRequest.substring(1));
    }

    /**
     * 规范化 URL 路径中的首尾与重复斜杠。
     *
     * @param path 原始路径
     * @return 以单斜杠开头的路径
     */
    private String normalizePath(String path) {
        if (path == null || path.isBlank() || "/".equals(path.trim())) {
            return "/";
        }
        String normalized = path.trim();
        if (!normalized.startsWith("/")) {
            normalized = "/" + normalized;
        }
        normalized = normalized.replaceAll("/{2,}", "/");
        return normalized.length() > 1 && normalized.endsWith("/")
                ? normalized.substring(0, normalized.length() - 1) : normalized;
    }

    /**
     * 构建顺序稳定的查询字符串。
     *
     * @param parameters 查询参数
     * @return 查询字符串，无参数时返回 null
     */
    private String buildQuery(Map<String, String> parameters) {
        if (parameters == null || parameters.isEmpty()) {
            return null;
        }
        return parameters.entrySet().stream()
                .filter(entry -> entry.getKey() != null && entry.getValue() != null)
                .sorted(Comparator.comparing(Map.Entry::getKey))
                .map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
                .collect(Collectors.joining("&"));
    }

    /**
     * 对单个查询参数执行 UTF-8 编码。
     *
     * @param value 参数值
     * @return 编码结果
     */
    private String encode(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
    }

    /**
     * 将远端 HTTP 状态映射为可恢复的业务错误。
     *
     * @param statusCode 远端 HTTP 状态码
     * @throws BusinessException 非 2xx 状态时抛出
     */
    private void validateStatus(int statusCode) {
        if (statusCode >= 200 && statusCode < 300) {
            return;
        }
        switch (statusCode) {
            case 401 -> throw new BusinessException(422, 42221, "API 密钥无效，请检查服务商配置");
            case 403 -> throw new BusinessException(422, 42222, "当前 API 密钥无权获取模型列表");
            case 404, 405 -> throw new BusinessException(422, 42223, "当前服务暂不支持获取模型列表");
            case 429 -> throw new BusinessException(429, 42911, "请求过于频繁，请稍后重试");
            default -> {
                if (statusCode >= 500) {
                    throw new BusinessException(502, 50221,
                            "模型服务暂时不可用（HTTP " + statusCode + "）");
                }
                throw new BusinessException(422, 42224,
                        "获取模型列表失败（HTTP " + statusCode + "）");
            }
        }
    }

    /**
     * 规范化供应商类型。
     *
     * @param value 原始供应商类型
     * @return 小写无首尾空白的供应商类型
     */
    private String normalize(String value) {
        return value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
    }
}
