package cclotus.weixin.util;

import cclotus.weixin.gateway.domain.AccessToken;
import cclotus.weixin.gateway.domain.Auth2Token;
import cclotus.weixin.gateway.domain.JsapiTicket;
import cclotus.weixin.gateway.domain.ReturnCode;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.codec.binary.Hex;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.util.DefaultUriBuilderFactory;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static cclotus.weixin.util.WeixinError.ERR_SERVICE_FAULT;

/** 公众平台通用接口工具类 */
@Slf4j
public class WeixinUtil {

	public final static String host = "https://api.weixin.qq.com";
	// 获取access_token的接口地址（GET） 限200（次/天）
	public final static String access_token_url_F = "/cgi-bin/token?appid={appid}&secret={secret}&grant_type=client_credential";
	public final static String auth2_token_url_F = "/sns/oauth2/access_token?appid={appid}&secret={secret}&code={code}&grant_type=authorization_code";
	public final static String jsapi_ticket_url = "/cgi-bin/ticket/getticket?access_token={token}&type=jsapi";

	/**
	 * 获取access_token
	 * @param appId 凭证
	 * @param secret 密钥
	 * @return AccessToken
	 */
	@NonNull
	public static Mono<AccessToken> getAccessToken(String appId, String secret) {
		// 构造请求Uri
		URI uri = new DefaultUriBuilderFactory()
				.expand(access_token_url_F, appId, secret);

		log.info("[WXAPI] 获取access_token for {}", appId);
		return apiGetAsync(uri, AccessToken.class).flatMap(ret -> {
			if(ret.getErrmsg() != null){
				log.error("接口调用失败[{}] errcode:{} errmsg:{}",
						uri,
						ret.getErrcode(),
						ret.getErrmsg());
				return Mono.error(new WeixinError(ret.getErrcode(), ret.getErrmsg()));
			}
			return Mono.just(ret);
		});
	}

	public static Mono<JsapiTicket> getJsapiTicket(String token) {
		// 构造请求Uri
		URI uri = new DefaultUriBuilderFactory()
				.expand(jsapi_ticket_url, token);

		log.info("[JSAPI] 获取ticket");
		return apiGetAsync(uri, JsapiTicket.class);
	}

	public static Mono<Auth2Token> getAuth2Token(String appId, String secret, String code) {
		// 构造请求Uri
		URI uri = new DefaultUriBuilderFactory()
				.expand(auth2_token_url_F, appId, secret, code);

		log.info("[OAUTH] 获取OAuth2 Token");
		return apiGetAsync(uri, Auth2Token.class);
	}

	@NonNull
	public static <T extends ReturnCode> T apiGet_OK(URI uri, Class<T> cls) throws WeixinError {
		T ret;
		String url = host + uri.toString();

		OkHttpClient client = new OkHttpClient();
		Request request = new Request.Builder()
				.url(url)
				.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
				.build();

		try (Response response = client.newCall(request).execute()) {
			String data = Objects.requireNonNull(response.body()).string();
			ret = JSON.parseObject(data, cls);
		} catch (Throwable e) {
			// 获取token失败
			log.error("接口调用失败[{}]",url, e);
			throw new WeixinError(ERR_SERVICE_FAULT, e.getMessage());
		}

		assert ret != null;
		if(ret.getErrmsg() != null){
			log.error("接口调用失败[{}] errcode:{} errmsg:{}",
					uri,
					ret.getErrcode(),
					ret.getErrmsg());
			throw new WeixinError(ret.getErrcode(), ret.getErrmsg());
		}
		return ret;
	}

	@NonNull
	public static <T extends ReturnCode> T apiGet(URI uri, Class<T> cls) throws WeixinError {
		T ret;
		try {
			ExecutorService executorService = Executors.newSingleThreadExecutor();
			// WebFlux异步调用，同步会报错
			Future<T> future = executorService.submit(() -> apiGetAsync(uri, cls)
					.block(Duration.of(30, ChronoUnit.SECONDS)));
			log.trace("[apiGet] start block: {}", uri);
			ret = future.get();
			log.trace("[apiGet] block finish: {}", uri);
		} catch (Throwable e) {
			// 获取token失败
			log.error("同步调用失败[{}]",uri, e);
			throw new WeixinError(ERR_SERVICE_FAULT, e.getMessage());
		}

		assert ret != null;
		if(ret.getErrmsg() != null){
			log.error("接口调用失败[{}] errcode:{} errmsg:{}",
					uri,
					ret.getErrcode(),
					ret.getErrmsg());
			throw new WeixinError(ret.getErrcode(), ret.getErrmsg());
		}
		return ret;
	}

	@NonNull
	public static <T extends ReturnCode> Mono<T> apiGetAsync(URI uri, Class<T> cls) {
		try {
			log.trace("[apiGetAsync] WebClient.create...");
			WebClient client = WebClient.create(host);

			log.trace("[apiGetAsync] client.retrieve...");
			return client.get()
					.uri(uri.toString()).accept(MediaType.APPLICATION_JSON)
					.retrieve()
					.bodyToMono(cls)
					.onErrorResume(error -> {
						log.error("接口调用失败[{}]: {}", uri, error.getMessage());
						try {
							T t = cls.getDeclaredConstructor().newInstance();
							t.setErrcode(ERR_SERVICE_FAULT);
							t.setErrmsg(error.getMessage());
							return Mono.just(t);
						} catch (Throwable e) {
							throw new RuntimeException(e);
						}
					});

		} catch (Throwable e) {
			// 获取token失败
			log.error("异步调用失败[{}]",uri, e);
			return Mono.error(new WeixinError(ERR_SERVICE_FAULT, e.getMessage()));
		}

//		assert ret != null;
//		if(ret.getErrmsg() != null){
//			log.error("接口调用失败[{}] errcode:{} errmsg:{}",
//					uri,
//					ret.getErrcode(),
//					ret.getErrmsg());
//			throw new WeixinError(ret.getErrcode(), ret.getErrmsg());
//		}
//		return ret;
	}

	public static String randStr(int length) {
		String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";// 含有字符和数字的字符串
		Random random = new Random();// 随机类初始化
		StringBuilder sb = new StringBuilder();// StringBuffer类生成，为了拼接字符串

		for (int i = 0; i < length; ++i) {
			int number = random.nextInt(str.length());
			sb.append(str.charAt(number));
		}
		return sb.toString();
	}

	public static String getSha1(String str) {
		MessageDigest md;
		try {
			md = MessageDigest.getInstance("SHA1");
			md.update(str.getBytes(StandardCharsets.UTF_8));
			return Hex.encodeHexString(md.digest());
		} catch (Exception e) {
			// Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

}
