package com.zcloud.personnel.positioning.integration.finds;

import com.alibaba.cola.exception.BizException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jjb.saas.framework.auth.model.SSOUser;
import com.jjb.saas.framework.auth.utils.AuthContext;
import com.zcloud.personnel.positioning.persistence.dataobject.OpenapiConfigDO;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * FindS OpenAPI signed client.
 *
 * @Author fangjiakai
 * @Date 2026-07-07
 */
@Component
@RequiredArgsConstructor
public class FindsOpenApiClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(FindsOpenApiClient.class);
    private static final String SIK_PATH_SEGMENT = "sik";

    @Value("${finds.openapi.connect-timeout-ms:10000}")
    private int connectTimeoutMs = 10000;

    @Value("${finds.openapi.read-timeout-ms:60000}")
    private int readTimeoutMs = 60000;

    @Value("${finds.openapi.max-request-attempts:3}")
    private int maxRequestAttempts = 3;

    @Value("${finds.openapi.fail-fast-connect-timeout-ms:5000}")
    private int failFastConnectTimeoutMs = 5000;

    @Value("${finds.openapi.fail-fast-read-timeout-ms:15000}")
    private int failFastReadTimeoutMs = 15000;

    private final OpenapiConfigCache openapiConfigCache;
    private final ObjectMapper objectMapper;

    public JsonNode post(String apiName, Map<String, Object> request) {
        OpenapiConfigDO config = loadEnabledConfig();
        return post(config, apiName, request);
    }

    public JsonNode postFailFast(String apiName, Map<String, Object> request) {
        OpenapiConfigDO config = loadEnabledConfig();
        return post(config, apiName, request,
                failFastConnectTimeoutMs, failFastReadTimeoutMs, 1);
    }

    public List<JsonNode> postFailFastBatch(String apiName, List<Map<String, Object>> requests,
                                            int maxConcurrency) {
        if (requests == null || requests.isEmpty()) {
            return Collections.emptyList();
        }
        OpenapiConfigDO config = loadEnabledConfig();
        int concurrency = Math.max(1, Math.min(Math.min(maxConcurrency, 8), requests.size()));
        if (concurrency == 1) {
            List<JsonNode> result = new ArrayList<>(requests.size());
            for (Map<String, Object> request : requests) {
                result.add(post(config, apiName, request,
                        failFastConnectTimeoutMs, failFastReadTimeoutMs, 1));
            }
            return result;
        }

        ExecutorService executor = Executors.newFixedThreadPool(concurrency);
        try {
            List<Future<JsonNode>> futures = new ArrayList<>(requests.size());
            for (Map<String, Object> request : requests) {
                futures.add(executor.submit(() -> post(config, apiName, request,
                        failFastConnectTimeoutMs, failFastReadTimeoutMs, 1)));
            }
            List<JsonNode> result = new ArrayList<>(requests.size());
            for (Future<JsonNode> future : futures) {
                result.add(future.get());
            }
            return result;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BizException("FindS OpenAPI批量请求被中断");
        } catch (ExecutionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            throw new BizException("FindS OpenAPI批量请求失败：" + cause.getMessage());
        } finally {
            executor.shutdownNow();
        }
    }

    public JsonNode post(OpenapiConfigDO config, String apiName, Map<String, Object> request) {
        return post(config, apiName, request, connectTimeoutMs, readTimeoutMs, maxRequestAttempts);
    }

    private JsonNode post(OpenapiConfigDO config, String apiName, Map<String, Object> request,
                          int connectTimeout, int readTimeout, int requestAttempts) {
        validateConfig(config);
        String requestJson = writeJson(request == null ? Collections.emptyMap() : request);
        String timestamp = String.valueOf(System.currentTimeMillis());
        String externalPrefix = normalizePrefix(defaultIfBlank(config.getExternalPrefix(), "/rest/"));
        String internalPrefix = normalizePrefix(defaultIfBlank(config.getInternalPrefix(), "/rest/"));
        String signPath = buildApiPath(externalPrefix, apiName, config.getSik());
        String requestPath = buildApiPath(internalPrefix, apiName, config.getSik());
        String signature = sign(signPath, requestJson, config.getSisCipherText(), timestamp);
        String url = trimTrailingSlash(config.getHostUrl()) + requestPath + "?_sign=" + signature;
        String response = doPost(url, requestJson, timestamp, apiName,
                connectTimeout, readTimeout, requestAttempts);
        try {
            return objectMapper.readTree(response);
        } catch (IOException e) {
            throw new BizException("FindS接口响应不是合法JSON：" + apiName);
        }
    }

    private OpenapiConfigDO loadEnabledConfig() {
        SSOUser currentUser = AuthContext.getCurrentUser();
        String companyId = currentUser == null || currentUser.getCompanyId() == null
                ? null : String.valueOf(currentUser.getCompanyId());
        OpenapiConfigDO config = openapiConfigCache.getEnabledConfig(companyId);
        if (config == null) {
            throw new BizException("未配置启用的FindS OpenAPI配置");
        }
        validateConfig(config);
        return config;
    }

    private void validateConfig(OpenapiConfigDO config) {
        if (!StringUtils.hasText(config.getHostUrl())) {
            throw new BizException("FindS OpenAPI hostUrl未配置");
        }
        if (!StringUtils.hasText(config.getSik())) {
            throw new BizException("FindS OpenAPI sik未配置");
        }
        if (!StringUtils.hasText(config.getSisCipherText())) {
            throw new BizException("FindS OpenAPI sis未配置");
        }
    }

    private String buildApiPath(String prefix, String apiName, String sik) {
        return prefix + apiName + "/" + SIK_PATH_SEGMENT + "/" + sik;
    }

    private String sign(String path, String requestJson, String sis, String timestamp) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(sis.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            mac.update(path.getBytes(StandardCharsets.UTF_8));
            mac.update("request".getBytes(StandardCharsets.UTF_8));
            mac.update(requestJson.getBytes(StandardCharsets.UTF_8));
            mac.update(timestamp.getBytes(StandardCharsets.UTF_8));
            return toUpperHex(mac.doFinal());
        } catch (Exception e) {
            throw new BizException("FindS OpenAPI签名失败");
        }
    }

    private String doPost(String url, String requestJson, String timestamp, String apiName,
                          int configuredConnectTimeout, int configuredReadTimeout,
                          int configuredRequestAttempts) {
        IOException lastException = null;
        int attempts = Math.max(configuredRequestAttempts, 1);
        int connectTimeout = Math.max(configuredConnectTimeout, 1);
        int readTimeout = Math.max(configuredReadTimeout, 1);
        for (int attempt = 1; attempt <= attempts; attempt++) {
            HttpURLConnection connection = null;
            long startedAt = System.nanoTime();
            try {
                byte[] body = ("request=" + URLEncoder.encode(requestJson, "UTF-8"))
                        .getBytes(StandardCharsets.UTF_8);
                connection = (HttpURLConnection) new URL(url).openConnection();
                connection.setConnectTimeout(connectTimeout);
                connection.setReadTimeout(readTimeout);
                connection.setRequestMethod("POST");
                connection.setDoOutput(true);
                connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
                connection.setRequestProperty("wz-acs-timestamp", timestamp);
                connection.setRequestProperty("Accept", "application/json");
                try (OutputStream outputStream = connection.getOutputStream()) {
                    outputStream.write(body);
                }
                int status = connection.getResponseCode();
                String response = readResponse(status >= 400
                        ? connection.getErrorStream() : connection.getInputStream());
                if (status != HttpURLConnection.HTTP_OK) {
                    throw new BizException("FindS OpenAPI HTTP调用失败，status=" + status + "，body=" + response);
                }
                LOGGER.debug("FindS OpenAPI request succeeded, api={}, attempt={}, elapsedMs={}",
                        apiName, attempt, elapsedMillis(startedAt));
                return response;
            } catch (IOException e) {
                lastException = e;
                LOGGER.warn("FindS OpenAPI request failed, api={}, attempt={}/{}, elapsedMs={}, message={}",
                        apiName, attempt, attempts, elapsedMillis(startedAt), e.getMessage());
                if (attempt < attempts) {
                    sleepBeforeRetry(attempt);
                }
            } finally {
                if (connection != null) {
                    connection.disconnect();
                }
            }
        }
        throw new BizException("FindS OpenAPI连接失败：" + (lastException == null
                ? "unknown error" : lastException.getMessage()));
    }

    private long elapsedMillis(long startedAt) {
        return (System.nanoTime() - startedAt) / 1_000_000L;
    }

    private void sleepBeforeRetry(int attempt) {
        try {
            Thread.sleep(500L * attempt);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BizException("FindS OpenAPI请求重试被中断");
        }
    }

    private String readResponse(InputStream inputStream) throws IOException {
        if (inputStream == null) {
            return "";
        }
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int len;
        while ((len = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, len);
        }
        return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
    }

    private String writeJson(Object value) {
        try {
            return objectMapper.writeValueAsString(value);
        } catch (IOException e) {
            throw new BizException("FindS OpenAPI请求JSON转换失败");
        }
    }

    private String toUpperHex(byte[] bytes) {
        char[] digits = "0123456789ABCDEF".toCharArray();
        char[] result = new char[bytes.length * 2];
        for (int i = 0; i < bytes.length; i++) {
            result[i * 2] = digits[(bytes[i] & 0xF0) >> 4];
            result[i * 2 + 1] = digits[bytes[i] & 0x0F];
        }
        return new String(result);
    }

    private String normalizePrefix(String prefix) {
        String value = prefix.trim();
        if (!value.startsWith("/")) {
            value = "/" + value;
        }
        if (!value.endsWith("/")) {
            value = value + "/";
        }
        return value;
    }

    private String trimTrailingSlash(String value) {
        String result = value.trim();
        while (result.endsWith("/")) {
            result = result.substring(0, result.length() - 1);
        }
        return result;
    }

    private String defaultIfBlank(String value, String defaultValue) {
        return StringUtils.hasText(value) ? value : defaultValue;
    }
}
