/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.nacos.core.utils;

import com.alibaba.nacos.common.constant.HttpHeaderConsts;
import com.alibaba.nacos.common.http.HttpUtils;
import com.alibaba.nacos.common.model.RestResult;
import com.alibaba.nacos.common.model.RestResultUtils;
import com.alibaba.nacos.sys.utils.DiskUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.multipart.MultipartFile;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;

import static com.alibaba.nacos.common.constant.HttpHeaderConsts.ACCEPT_ENCODING;
import static com.alibaba.nacos.common.http.param.MediaType.APPLICATION_JSON;

/**
 * web utils.
 *
 * @author nkorange
 */
public class WebUtils {
    
    private static final String ENCODING_KEY = "encoding";
    
    private static final String COMMA = ",";
    
    private static final String SEMI = ";";
    
    private static final String TMP_SUFFIX = ".tmp";
    
    private static final String X_REAL_IP = "X-Real-IP";
    
    private static final String X_FORWARDED_FOR = "X-Forwarded-For";
    
    private static final String X_FORWARDED_FOR_SPLIT_SYMBOL = ",";
    
    /**
     * get target value from parameterMap, if not found will throw {@link IllegalArgumentException}.
     *
     * @param req {@link HttpServletRequest}
     * @param key key
     * @return value
     */
    public static String required(final HttpServletRequest req, final String key) {
        String value = req.getParameter(key);
        if (StringUtils.isEmpty(value)) {
            throw new IllegalArgumentException("Param '" + key + "' is required.");
        }
        String encoding = req.getParameter(ENCODING_KEY);
        return resolveValue(value, encoding);
    }
    
    /**
     * get target value from parameterMap, if not found will return default value.
     *
     * @param req          {@link HttpServletRequest}
     * @param key          key
     * @param defaultValue default value
     * @return value
     */
    public static String optional(final HttpServletRequest req, final String key, final String defaultValue) {
        String value = req.getParameter(key);
        if (StringUtils.isBlank(value)) {
            return defaultValue;
        }
        String encoding = req.getParameter(ENCODING_KEY);
        return resolveValue(value, encoding);
    }
    
    /**
     * decode target value.
     *
     * @param value    value
     * @param encoding encode
     * @return Decoded data
     */
    private static String resolveValue(String value, String encoding) {
        if (StringUtils.isEmpty(encoding)) {
            encoding = StandardCharsets.UTF_8.name();
        }
        try {
            value = new String(value.getBytes(StandardCharsets.UTF_8), encoding);
        } catch (UnsupportedEncodingException ignore) {
        }
        return value.trim();
    }
    
    /**
     * decode target value with UrlDecode.
     *
     * <p>Under Content-Type:application/x-www-form-urlencoded situation.
     *
     * @param value    value
     * @param encoding encode
     * @return Decoded data
     */
    private static String resolveValueWithUrlDecode(String value, String encoding) {
        if (StringUtils.isEmpty(encoding)) {
            encoding = StandardCharsets.UTF_8.name();
        }
        try {
            value = HttpUtils.decode(new String(value.getBytes(StandardCharsets.UTF_8), encoding), encoding);
        } catch (UnsupportedEncodingException ignore) {
        } catch (Exception ex) {
            // If the value contains a special character without encoding (such as "[IPv6]"),
            // a URLDecoder exception is thrown, which is ignored and the original value is returned
            final String seq = "URLDecoder";
            if (!StringUtils.contains(ex.toString(), seq)) {
                throw ex;
            }
        }
        return value.trim();
    }
    
    /**
     * get accept encode from request.
     *
     * @param req {@link HttpServletRequest}
     * @return accept encode
     */
    public static String getAcceptEncoding(HttpServletRequest req) {
        String encode = StringUtils.defaultIfEmpty(req.getHeader(ACCEPT_ENCODING), StandardCharsets.UTF_8.name());
        encode = encode.contains(COMMA) ? encode.substring(0, encode.indexOf(COMMA)) : encode;
        return encode.contains(SEMI) ? encode.substring(0, encode.indexOf(SEMI)) : encode;
    }
    
    /**
     * Returns the value of the request header "user-agent" as a <code>String</code>.
     *
     * @param request HttpServletRequest
     * @return the value of the request header "user-agent", or the value of the request header "client-version" if the
     * request does not have a header of "user-agent".
     */
    public static String getUserAgent(HttpServletRequest request) {
        String userAgent = request.getHeader(HttpHeaderConsts.USER_AGENT_HEADER);
        if (StringUtils.isEmpty(userAgent)) {
            userAgent = StringUtils
                    .defaultIfEmpty(request.getHeader(HttpHeaderConsts.CLIENT_VERSION_HEADER), StringUtils.EMPTY);
        }
        return userAgent;
    }
    
    /**
     * response data to client.
     *
     * @param response {@link HttpServletResponse}
     * @param body     body
     * @param code     http code
     * @throws IOException IOException
     */
    public static void response(HttpServletResponse response, String body, int code) throws IOException {
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        response.setContentType(APPLICATION_JSON);
        response.getWriter().write(body);
        response.setStatus(code);
    }
    
    /**
     * Handle file upload operations.
     *
     * @param multipartFile file
     * @param consumer      post processor
     * @param response      {@link DeferredResult}
     */
    public static void onFileUpload(MultipartFile multipartFile, Consumer<File> consumer,
            DeferredResult<RestResult<String>> response) {
        
        if (Objects.isNull(multipartFile) || multipartFile.isEmpty()) {
            response.setResult(RestResultUtils.failed("File is empty"));
            return;
        }
        File tmpFile = null;
        try {
            tmpFile = DiskUtils.createTmpFile(multipartFile.getName(), TMP_SUFFIX);
            multipartFile.transferTo(tmpFile);
            consumer.accept(tmpFile);
        } catch (Throwable ex) {
            if (!response.isSetOrExpired()) {
                response.setResult(RestResultUtils.failed(ex.getMessage()));
            }
        } finally {
            DiskUtils.deleteQuietly(tmpFile);
        }
    }
    
    /**
     * Register DeferredResult in the callback of CompletableFuture.
     *
     * @param deferredResult {@link DeferredResult}
     * @param future         {@link CompletableFuture}
     * @param errorHandler   {@link Function}
     * @param <T>            target type
     */
    public static <T> void process(DeferredResult<T> deferredResult, CompletableFuture<T> future,
            Function<Throwable, T> errorHandler) {
        
        deferredResult.onTimeout(future::join);
        
        future.whenComplete((t, throwable) -> {
            if (Objects.nonNull(throwable)) {
                deferredResult.setResult(errorHandler.apply(throwable));
                return;
            }
            deferredResult.setResult(t);
        });
    }
    
    /**
     * Register DeferredResult in the callback of CompletableFuture.
     *
     * @param deferredResult {@link DeferredResult}
     * @param future         {@link CompletableFuture}
     * @param success        if future success, callback runnable
     * @param errorHandler   {@link Function}
     * @param <T>            target type
     */
    public static <T> void process(DeferredResult<T> deferredResult, CompletableFuture<T> future, Runnable success,
            Function<Throwable, T> errorHandler) {
        
        deferredResult.onTimeout(future::join);
        
        future.whenComplete((t, throwable) -> {
            if (Objects.nonNull(throwable)) {
                deferredResult.setResult(errorHandler.apply(throwable));
                return;
            }
            success.run();
            deferredResult.setResult(t);
        });
    }
    
    /**
     * get real client ip
     *
     * <p>first use X-Forwarded-For header    https://zh.wikipedia.org/wiki/X-Forwarded-For next nginx X-Real-IP last
     * {@link HttpServletRequest#getRemoteAddr()}
     *
     * @param request {@link HttpServletRequest}
     * @return remote ip address.
     */
    public static String getRemoteIp(HttpServletRequest request) {
        String xForwardedFor = request.getHeader(X_FORWARDED_FOR);
        if (!StringUtils.isBlank(xForwardedFor)) {
            return xForwardedFor.split(X_FORWARDED_FOR_SPLIT_SYMBOL)[0].trim();
        }
        String nginxHeader = request.getHeader(X_REAL_IP);
        return StringUtils.isBlank(nginxHeader) ? request.getRemoteAddr() : nginxHeader;
    }
}
