package cclotus.weixin.gateway;

import cclotus.weixin.gateway.domain.AuthState;
import cclotus.weixin.gateway.service.CacheService;
import cclotus.weixin.gateway.service.TokenService;
import cclotus.weixin.util.WeixinError;
import cclotus.weixin.util.WeixinUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.webflux.ProxyExchange;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.result.view.Rendering;
import org.springframework.web.util.DefaultUriBuilderFactory;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.*;

import static cclotus.weixin.util.WeixinError.*;
import static cclotus.weixin.util.WeixinUtil.getSha1;
import static cclotus.weixin.util.WeixinUtil.randStr;
import static org.springframework.util.Assert.hasText;
import static org.springframework.util.Assert.isTrue;

@SuppressWarnings({"SpellCheckingInspection", "unused", "rawtypes"})
@Slf4j
@Controller
public class WeixinController {

    @Autowired
    private CacheService cache;

    // json object mapper
    private final ObjectMapper mapper = new ObjectMapper();

    @Autowired
    private TokenService service;

    @Autowired
    private WeixinConfigure config;

    /**
     * 微信接口代理（后端调用）
     * @param path    接口地址路径
     * @param appid   公众号/小程序appId
     * @param force   是否强制刷新access_token
     */
    @RequestMapping("/api.weixin.qq.com/{*path}")
    public Mono<? extends ResponseEntity<?>> wxapi(@PathVariable String path, String appid, String force, ServerHttpRequest request, ProxyExchange<byte[]> proxy) throws WeixinError {
        hasText(path, "接口路径无效");
        hasText(appid, "appid不能为空");

        log.info("[WXAPI] {} {}?{}", request.getMethod(), request.getURI().getRawPath(), request.getURI().getRawQuery());

        // 禁止访问token接口
        if (path.endsWith("/token")) {
            log.error("can't call get token");
            return Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST).body("can't call get token"));
        }

        // 清空缓存，重新请求token
        if ("1".equals(force) || "true".equalsIgnoreCase(force)) {
            service.clearToken(appid);
        }

        // 构造请求参数
        boolean forSecret = config.getNoTokenApi().contains(path);
        String name = forSecret ? "secret" : "access_token";
        // 获取token
        if (forSecret) {
            String token = config.getSecret().getOrDefault(appid, null);
            if (token == null) throw new WeixinError(ERR_APPID_INVALID);
        }

        Mono<String> tokenMono = service.fetchToken(appid);

        return tokenMono.flatMap(token -> {
            // 构造请求Uri
            String uri = new DefaultUriBuilderFactory()
                    .uriString(WeixinUtil.host)
                    .path(path)
                    .queryParam(name, token)
                    .queryParams(request.getQueryParams())
                    .build().toString();

            // 转发接口调用请求
            return proxy.uri(uri).forward();
        });
    }

    /**
     * jssdk签名（前端调用）
     * @param url   请求页面原始Url
     * @param appid 公众号/小程序appId
     */
    @GetMapping("/wxapi/jssign")
    @ResponseBody
    public Mono<ResponseEntity<?>> jssign(@RequestHeader("referer") String url, String appid, String force, ServerHttpRequest request) {
        hasText(appid, "appid不能为空");
        isTrue(!appid.matches(".*(<|>|script|eval|document).*"),"invalid appid");

        try {
            // 请求ticket
            return service.fetchTicket(appid).map(ticket -> {
                String noncestr = randStr(16);
                String timestamp = String.valueOf(System.currentTimeMillis());
                String prodata = String.format("jsapi_ticket=%s&noncestr=%s&timestamp=%s&url=%s", ticket,
                        noncestr, timestamp, url);
                String sign = getSha1(prodata);

                LinkedHashMap ret = new LinkedHashMap<String, Object>() {{
                    put("errcode", 0);
                    put("errmsg", "ok");
                    put("noncestr", noncestr);
                    put("prodata", prodata);
                    put("sign", sign);
                }};

                // 生成签名信息
                return ResponseEntity.ok(ret);
            });
        } catch (WeixinError e) {
            return Mono.just(ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(e.getMessage()));
        }

    }

    /**
     * 页面授权
     * @param appid 公众号/小程序appId
     * @param redirect_uri 授权成功后跳转地址
     * @param response_type 返回类型（此处定义与微信接口原始的response_type有所差别）
     *          code - 生成随机UUID作为缓存Key，返回该code，应用后端通过code调用fetch接口获取真实openid
     *          openid - 直接将openid附加到请求的回调地址，默认
     */
    @ConditionalOnProperty(prefix = "wxapi.gateway", name = "auth-base-url")
    @GetMapping(value = "/wxapi/auth", params = "redirect_uri")
    public Rendering auth(String appid, String redirect_uri, String response_type) throws UnsupportedEncodingException, JsonProcessingException {
        hasText(appid, "appid不能为空");
        isTrue(!appid.matches(".*(<|>|script|eval|document).*"),"invalid appid");

        log.info("request authorize for [{}]",redirect_uri);
        String state = UUID.randomUUID().toString();

        String baseUrl = config.getAuthBaseUrl();
        hasText(baseUrl, "authBaseUrl尚未配置");
        String callbackUrl = String.format("%s/wxapi/auth?appid=%s", baseUrl, appid);
        String callback = URLEncoder.encode(callbackUrl, "UTF-8");

        // 缓存认证状态
        String json = mapper.writeValueAsString(new AuthState(appid, redirect_uri, response_type));
        cache.setCache(stateKey(state), json, config.getStateTimeout());

        // 重定向到微信认证地址
        String url = String.format("https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=%s#wechat_redirect",appid,callback,state);
        log.info("redirect to: {}", url);
        return Rendering.redirectTo(url).build();
    }

    /**
     * 页面授权回调
     * @param code 凭证码，用于换取openid
     * @param state 状态码，关联原始认证请求
     * @return 重定向到原始请求页面，query参数附加上openid（详见auth接口response_type参数说明）
     */
    @ConditionalOnProperty(prefix = "wxapi.gateway", name = "auth-base-url")
    @GetMapping(value = "/wxapi/auth", params = "!redirect_uri")
    public Mono<Rendering> callback(String code, String state) throws WeixinError {
        hasText(code, "invalid param");
        hasText(state, "invalid param");

        log.info("authorize callback ...");

        // 从缓存获取状态数据
        AuthState authState;
        try{
            String json = cache.getCache(stateKey(state));
            if (json == null) throw new WeixinError(ERR_STATE_INVALID);
            authState = mapper.readValue(json, AuthState.class);
        }catch(JsonProcessingException e){
            log.error("restore auth state fail!", e);
            throw new WeixinError(ERR_STATE_INVALID, e);
        }

        String response_type = authState.getType();
        String appid = authState.getAppid();

        //通过code获得用户openid
        return service.fetchOpenid(appid, code).map(openid -> {
            // 构造跳转地址
            String to_uri = authState.getUrl();
            String part = "&openid=" + openid;
            if("code".equalsIgnoreCase(response_type)){
                String _code = UUID.randomUUID().toString();
                cache.setCache(codeKey(_code), openid, config.getCodeTimeout());
                part += "&code=" + _code;
            }
            if(!to_uri.contains("?")){
                part = part.replaceFirst("&", "?");
            }
            to_uri += part;

            // 重定向到原始请求页面
            return Rendering.redirectTo(to_uri).build();
        });
    }

    /**
     * 通过code获取openid（后端调用）
     */
    @GetMapping("/wxapi/fetch")
    @ResponseBody
    public ResponseEntity<?> fetch(String code) throws WeixinError {
        hasText(code, "invalid param");
        log.info("fetch openid ...");

        String openid = cache.getCache(codeKey(code));
        if (openid == null) {
            throw new WeixinError(ERR_CODE_INVALID);
        }

        Map<String, Object> ret = new HashMap<String, Object>() {{
            put("errcode", 0);
            put("errmsg", "ok");
            put("openid", openid);
        }};
        return ResponseEntity.ok(ret);
    }

    /**
     * 功能演示
     */
    @GetMapping("/wxapi/demo")
    @ResponseBody
    public ResponseEntity<?> demo(String code, String openid) {
        Map<String, String> ret = new HashMap<String, String>() {{
            put("code", code);
            put("openid", openid);
        }};
        return ResponseEntity.ok(ret);
    }

    public static String stateKey(String state) {
        return String.format("wxapi:auth:state:%s", state);
    }

    public static String codeKey(String code) {
        return String.format("wxapi:auth:code:%s", code);
    }

    @ExceptionHandler(Throwable.class)
    @ResponseBody
    private ResponseEntity<String> handleError(Throwable ex, ServerHttpRequest request) {
        log.warn("[WXAPI] ERROR: {}", ex.getMessage());
        if(log.isTraceEnabled()) {
            ex.printStackTrace();
        }
        String msg = ex.getMessage();
        if (msg == null) msg = "系统错误: " + ex.getClass().getName();
        List<MediaType> accept = request.getHeaders().getAccept();
        boolean isJson = accept.contains(MediaType.APPLICATION_JSON) || accept.contains(MediaType.ALL);
        if (isJson) {
            int errcode = -1;
            if (ex instanceof WeixinError) {
                msg = ((WeixinError) ex).toJsonString();
            } else {
                if (ex instanceof IllegalArgumentException) {
                    errcode = ERR_BADPARAM;
                }
                msg = String.format("{ \"errcode\": %d, \"errmsg\": \"%s\" }", errcode, msg);
            }
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(msg);
        }
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(msg);
    }
}
