package com.jsowell.thirdparty.amap.util;

import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.io.*;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.*;

/**
 * 高德地图工具类
 */
@Component
public class AMapUtils {
    private static String MERCHANT_PRIVATE_KEY;
    private static String AMAP_PUBLIC_KEY;
    private static String APP_ID;
    private static String DEV_URL;
    private static String PRD_URL;

    @Value("${aMap.merchantPrivateKey}")
    public void setMerchantPrivateKey(String merchantPrivateKey) {
        AMapUtils.MERCHANT_PRIVATE_KEY = merchantPrivateKey;
    }

    @Value("${aMap.aMapPublicKey}")
    public void setAmapPublicKey(String amapPublicKey) {
        AMapUtils.AMAP_PUBLIC_KEY = amapPublicKey;
    }

    @Value("${aMap.appId}")
    public void setAppId(String appId) {
        AMapUtils.APP_ID = appId;
    }

    @Value("${aMap.devUrl}")
    public void setDevUrl(String devUrl) {
        AMapUtils.DEV_URL = devUrl;
    }

    @Value("${aMap.prdUrl}")
    public void setPrdUrl(String prdUrl) {
        AMapUtils.PRD_URL = prdUrl;
    }

    /**
     * 使用商家私钥生成签名
     *
     * @param paramMap
     * @return
     * @throws Exception
     */
    public static String generateSign(Map<String, String> paramMap) throws Exception {
        //使用商家私钥进行加签，请在高德云店「接入准备及配置」页面生成并获取商家私钥
        String signContent = getSignContent(paramMap);
        return getSign(signContent, MERCHANT_PRIVATE_KEY);
    }

    /**
     * 构造参数map
     * @param bizContent 业务参数
     * @param SPI 高德文档中每个接口的SPI
     * @return 构造好可直接发送请求的map参数
     */
    public static Map<String, String> generateParamMap(JSONObject bizContent, String SPI) throws Exception {
        Map<String, String> map = new LinkedHashMap<>();
        map.put("biz_content", JSONObject.toJSONString(bizContent));
        map.put("app_id", APP_ID);
        map.put("utc_timestamp", String.valueOf(System.currentTimeMillis()));
        map.put("version", "1.0");
        map.put("charset", "UTF-8");
        map.put("method", SPI);
        // 生成签名
        String sign = AMapUtils.generateSign(map);
        map.put("sign", sign);
        map.put("sign_type", "RSA2");

        return map;
    }

    /**
     * 向高德地图发送请求
     * @param paramMap 请求参数
     * @return 高德返回结果
     */
    public static String sendPost(Map<String, String> paramMap) {
        // 定义Content-type、初始化HttpEntity
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, Object> formBody = convertToMultiValueMap(paramMap);
        HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(formBody, headers);

        // 调用高德接口
        // 线下联调环境：https://restapi.amap.com/rest/openmp/devgw?key=高德云店秘钥key
        // 线上环境：https://restapi.amap.com/rest/openmp/gw?key=高德云店秘钥key
        // 高德云店秘钥key请登录高德云店「接入准备及配置」页面查看
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> resp = restTemplate.postForEntity(DEV_URL, entity, String.class);
        if (resp.getBody() != null && !"".equals(resp.getBody())) {
            JSONObject respObj = JSONObject.parseObject(resp.getBody());
            System.out.println("高德返回结果:" + respObj);
            return JSONObject.toJSONString(respObj);
        }
        return null;
    }

    /**
     * 将Map转为MultiValueMap
     * （MultiValueMap 可一个 key 对应多个 value）
     * @param paramMap 业务参数map
     * @return MultiValueMap
     */
    private static MultiValueMap<String, Object> convertToMultiValueMap(Map<String, String> paramMap) {
        MultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap<>();
        for (Map.Entry<String, String> entry : paramMap.entrySet()) {
            multiValueMap.add(entry.getKey(), entry.getValue());
        }
        return multiValueMap;
    }

    /**
     * 参数转换为待加签字符串
     *
     * @param paramMap 待生成加密sign的参数集合
     */
    private static String getSignContent(Map<String, String> paramMap) {
        StringBuilder content = new StringBuilder();
        List<String> keys = new ArrayList<>(paramMap.keySet());
        // 将参数集合排序
        Collections.sort(keys);
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            //排除不需要参与签名的公共参数
            if ("sign_type".equals(key) || "sign".equals(key) || "need_encrypt".equals(key)) {
                continue;
            }
            String value = paramMap.get(key);
            // 拼装所有非空参数
            if (key != null && !"".equalsIgnoreCase(key) && value != null && !"".equalsIgnoreCase(value)) {
                content.append(i == 0 ? "" : "&").append(key).append("=").append(value);
            }
        }
        return content.toString();
    }

    /**
     * 字符串加签
     *
     * @param signContent        待加密的参数字符串
     * @param merchantPrivateKey 商家应用私钥
     * @throws IOException
     * @throws GeneralSecurityException
     */
    private static String getSign(String signContent, String merchantPrivateKey) throws IOException, GeneralSecurityException {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        byte[] encodedKey = readText(new ByteArrayInputStream(merchantPrivateKey.getBytes())).getBytes();
        encodedKey = Base64.getDecoder().decode(encodedKey);
        PrivateKey priKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey));

        Signature signature = Signature.getInstance("SHA256WithRSA");
        signature.initSign(priKey);
        signature.update(signContent.getBytes("UTF-8"));
        byte[] signed = signature.sign();
        return new String(Base64.getEncoder().encode(signed));
    }

    private static String readText(InputStream in) throws IOException {
        Reader reader = new InputStreamReader(in);
        StringWriter writer = new StringWriter();

        int bufferSize = 4096;
        char[] buffer = new char[bufferSize];
        int amount;
        while ((amount = reader.read(buffer)) >= 0) {
            writer.write(buffer, 0, amount);
        }
        return writer.toString();
    }


    // =====================================验签==============================================

    /**
     * 使用高德公钥验证签名
     *
     * @param paramFromAmap
     * @return
     * @throws Exception
     */
    public static boolean checkSign(Map<String, String> paramFromAmap) throws Exception {
        //使用高德公钥进行验签，请在高德云店「API对接信息」页面直接获取高德公钥
        String signContent = getSignContent(paramFromAmap);
        return checkSign(signContent, paramFromAmap.get("sign"), AMAP_PUBLIC_KEY);
    }

    /**
     * 对加密字符串进行验签
     *
     * @param content       待验签内容
     * @param sign          待验证签名
     * @param amapPublicKey 高德公钥
     * @throws IOException
     * @throws GeneralSecurityException
     */
    private static boolean checkSign(String content, String sign, String amapPublicKey) throws IOException, GeneralSecurityException {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        StringWriter writer = new StringWriter();
        io(new InputStreamReader(new ByteArrayInputStream(amapPublicKey.getBytes())), writer);
        byte[] encodedKey = writer.toString().getBytes();
        encodedKey = org.apache.commons.codec.binary.Base64.decodeBase64(encodedKey);
        PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
        Signature signature = Signature.getInstance("SHA256WithRSA");
        signature.initVerify(pubKey);
        signature.update(content.getBytes("UTF-8"));
        return signature.verify(Base64.getDecoder().decode(sign.getBytes()));
    }

    public static void io(Reader in, Writer out) throws IOException {
        int bufferSize = 4096;
        char[] buffer = new char[bufferSize];
        int amount;
        while ((amount = in.read(buffer)) >= 0) {
            out.write(buffer, 0, amount);
        }
    }
}
