package com.sonic.common.config;

import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.sonic.common.AppRuntime;
import com.sonic.common.auth.AuthCallerResolver;
import com.sonic.common.auth.OriginalCallerResolver;
import com.sonic.common.auth.SessionResolver;
import com.sonic.common.context.HttpRequestContext;
import com.sonic.common.context.RequestContextInterceptor;
import com.sonic.common.exception.AbstractExceptionHandler;
import com.sonic.common.log.TraceInterceptor;
import com.sonic.common.utils.DateConvertUtils;
import com.sonic.common.utils.FastjsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.yaml.snakeyaml.Yaml;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.parser.deserializer.Jdk8DateCodec;
import com.alibaba.fastjson.serializer.SerializeConfig;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;

import lombok.extern.slf4j.Slf4j;

/**
 * @author code
 */
@Slf4j
@Import({DefaultWebMvcConfig.GlobalExceptionHandler.class})
public class DefaultWebMvcConfig extends DelegatingWebMvcConfiguration implements InitializingBean {
    private static final String ACCESS_CONTROL_MAX_AGE = String.valueOf(TimeUnit.DAYS.toSeconds(1));
    @Autowired
    private DispatcherServlet servlet;

    @Autowired
    private AppRuntime appRuntime;

    // XXX：handlerInterceptors可以为空，不做非空约束
    @Autowired(required = false)
    private HandlerInterceptor[] handlerInterceptors;

    @Autowired(required = false)
    private HandlerMethodArgumentResolver[] handlerMethodArgumentResolvers;

    //该validator由ValidationAutoConfiguration提供.
    @Autowired(required = false)
    @Qualifier(value = "defaultValidator")
    Validator validator;

    /** 系统使用的时区 (默认为 CTT 中国上海时区)*/
    @Value("${shortZoneId.system:CTT}")
    private String systemShortZoneId;

    /** 需要转换的时区 (默认转换为 PST美国洛杉矶时区)*/
    @Value("${shortZoneId.convert:PST}")
    private String convertShortZoneId;

    /**
     * 配置对http request，response的converter
     * 1. 使用fastjson来序列化/反序列化json string
     * 2. 全局配置fastjson对LocalDateTime, LocalDate的序列化/反序列化
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(FastjsonUtils.SERIALIZER_FEATURES);

        // 支持Actuator. Actuator返回的数据content-type是application/vnd.spring-boot.actuator.v2+json
        MediaType application = new MediaType("application", "*+json");

        FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter();
        fastJsonHttpMessageConverter.setSupportedMediaTypes(Lists.newArrayList(MediaType.APPLICATION_JSON_UTF8, application));
        fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
        converters.add(0, fastJsonHttpMessageConverter);

        // 适用于直接原文返回数据的response @RequestMapping(value = "/xxx", produces = "text/plain;charset=utf-8")
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        stringHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_PLAIN));
        converters.add(1, stringHttpMessageConverter);

        // support yml
        YamlHttpMessageConverter yamlHttpMessageConverter = new YamlHttpMessageConverter();
        yamlHttpMessageConverter.setSupportedMediaTypes(Arrays.asList(YamlHttpMessageConverter.MEDIA_TYPE_YAML, YamlHttpMessageConverter.MEDIA_TYPE_YML));
        converters.add(2, yamlHttpMessageConverter);

        initFastJsonGlobalConfig();
    }

    private void initFastJsonGlobalConfig() {
        // 全局配置, 为LocalDateTime添加专用的serializer
        SerializeConfig.getGlobalInstance().put(LocalDateTime.class, (serializer, object, fieldName, fieldType, features) -> {
            if (object == null) {
                serializer.out.writeNull();
                return;
            }
            long value = ((LocalDateTime) object).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
            serializer.out.writeLong(value);
        });

        // 全局配置, 为LocalDate添加专用的serializer
        SerializeConfig.getGlobalInstance().put(LocalDate.class, (serializer, object, fieldName, fieldType, features) -> {
            if (object == null) {
                serializer.out.writeNull();
                return;
            }
            long value = ((LocalDate) object).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
            serializer.out.writeLong(value);
        });

        // 初始状态的ParserConfig.deserializers中没有LocalDateTime.class对应的deserializer
        // 会造成直接从timestamp反序列化到LocalDateTime失败；需要预设成默认的Jdk8DateCodec
        ParserConfig.getGlobalInstance().putDeserializer(LocalDateTime.class, Jdk8DateCodec.instance);
    }

    /**
     * 配置formatter，将API入参中的timestamp转化为LocalDateTime或者LocalDate
     *
     * @param registry
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        super.addFormatters(registry);
        //XXX: 不能使用lambda
        registry.addConverter(new Converter<String, LocalDateTime>() {
            @Override
            public LocalDateTime convert(String source) {
                if (Strings.isNullOrEmpty(source)) {
                    return null;
                }
                return LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(source)), ZoneId.systemDefault());
            }
        });

        registry.addConverter(new Converter<String, LocalDate>() {
            @Override
            public LocalDate convert(String source) {
                if (Strings.isNullOrEmpty(source)) {
                    return null;
                }
                return Instant.ofEpochMilli(Long.parseLong(source)).atZone(ZoneId.systemDefault()).toLocalDate();
            }
        });
    }

    /**
     * 添加拦截器
     *
     * @param registry
     */
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TraceInterceptor(appRuntime));
        // 跨域的拦截器, 并设置最高优先级. 避免抛出异常时跨域未生效
        // 不建议使用DelegatingWebMvcConfiguration.addCorsMappings. 使用该方法配置之后再使用自定义拦截器时跨域相关配置就会失效
        registry.addInterceptor(new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                //优先使用请求传入的ORIGIN, 参考org.springframework.web.cors.DefaultCorsProcessor#handleInternal
                String origin = StringUtils.firstNonBlank(request.getHeader(HttpHeaders.ORIGIN), CorsConfiguration.ALL);
                if (response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN).isEmpty()) {
                    response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
                }
                // 前端手动设置了 headers: {content-type: 'application/json'}，在 OPTIONS 预请求的时候需要指定允许
                // 反向代理后，需要设置auth-tk
                if (response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS).isEmpty()) {
                    response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "content-type,auth-tk,*");
                }
                response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, CorsConfiguration.ALL);
                if (response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS).isEmpty()) {
                    response.addHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE.toString());
                }
                response.addHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_MAX_AGE);
                return true;
            }
        });

        super.addInterceptors(registry);

        if (null != handlerInterceptors) {
            Arrays.stream(handlerInterceptors).forEach(e -> {
                registry.addInterceptor(e);
            });
        }
        registry.addInterceptor(new RequestContextInterceptor());
    }

    /**
     * 添加ArgumentResolver，以便在Controller中直接获取argument
     *
     * @param argumentResolvers
     */
    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        super.addArgumentResolvers(argumentResolvers);
        if (null != handlerMethodArgumentResolvers) {
            Arrays.stream(handlerMethodArgumentResolvers).forEach(e -> {
                argumentResolvers.add(e);
            });
        }
        //XXX 用于controller方法参数列表直接获取AuthCaller
        argumentResolvers.add(new AuthCallerResolver());
        argumentResolvers.add(new OriginalCallerResolver());
        argumentResolvers.add(new SessionResolver());
    }

    /**
     * 显示指定spring mvc使用的validator, 避免在方法签名中使用@Validated不生效
     *
     * @return
     */
    @Override
    protected Validator getValidator() {
        return Optional.ofNullable(validator).orElse(super.getValidator());
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        //设置找不到mapping时抛出异常, 避免直接返回错误页面
        servlet.setThrowExceptionIfNoHandlerFound(true);

        //初始化数据到时间转换工具类中
        DateConvertUtils.systemShortZoneId = systemShortZoneId;
        DateConvertUtils.convertShortZoneId = convertShortZoneId;
    }

    /**
     * 全局的异常处理，当API调用抛出异常后，由该GlobalExceptionHandler处理
     */
    @ControllerAdvice
    static class GlobalExceptionHandler extends AbstractExceptionHandler {
        public GlobalExceptionHandler(ExceptionHandlerHook hook) {
            super(hook);
        }
    }

    /**
     * RequestContext保存当前Http Request
     *
     * @return RequestContext
     */
    @Bean
    public HttpRequestContext requestContext() {
        return new HttpRequestContext();
    }

    /**
     * 获取当前服务AppId的Hook
     *
     * @param appRuntime
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    AbstractExceptionHandler.ExceptionHandlerHook exceptionHandlerHook(AppRuntime appRuntime) {
        return () -> appRuntime.getAppId();
    }

    /**
     * copy from https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/yaml-msg-converter.html
     * @param <T>
     */
    private static class YamlHttpMessageConverter<T> extends AbstractHttpMessageConverter<T> {
        static final MediaType MEDIA_TYPE_YAML = MediaType.valueOf("text/yaml");
        static final MediaType MEDIA_TYPE_YML = MediaType.valueOf("text/yml");

        @Override
        protected boolean supports (Class<?> clazz) {
            return true;
        }

        @Override
        protected T readInternal (Class<? extends T> clazz, HttpInputMessage inputMessage)
                throws IOException, HttpMessageNotReadableException {
            Yaml yaml = new Yaml();
            Iterable<Object> itr = yaml.loadAll(inputMessage.getBody());

            Object itrNext = itr.iterator().next();
            if (itrNext instanceof Collection) {
                return (T) new JSONArray().fluentAddAll((Collection) itrNext).toJavaList(JSONObject.class);
            }

            // find the root which is a map
            return new JSONObject((Map)itrNext).toJavaObject(clazz);
        }

        @Override
        protected void writeInternal (T t, HttpOutputMessage outputMessage)
                throws HttpMessageNotWritableException {
            throw new UnsupportedOperationException("not convert object to yml");
        }
    }

    /**
     * 发现如果继承了WebMvcConfigurationSupport，则在yml中配置的相关内容会失效。 需要重新指定静态资源
     *
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // swagger 资源
        registry.addResourceHandler("/**").addResourceLocations(
                "classpath:/static/");
        registry.addResourceHandler("swagger-ui.html").addResourceLocations(
                "classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations(
                "classpath:/META-INF/resources/webjars/");
        // 有什么需要自测的网页，可以放在这个目录下
        registry.addResourceHandler("/**").addResourceLocations(
                "classpath:/templates/");
        super.addResourceHandlers(registry);
    }
}
