/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.ambari.server.api;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.ClassPath;


/**
 * Filter to work around the original limitation of the REST API implementation of Amabri,
 * where the Content-Type is always text/plain, despite the accepted/returned JSON content.
 * When the request uses application/json as Content-Type, Ambari fails.
 *
 * This workaround is replacing application/json Content-Type to text/plain in requests, thus
 * preventing the failure.
 *
 * Furthermore the response is also tweaked by changing the Content-Type to application/json
 * when the original Content-Type of the request was application/json.
 */
public class ContentTypeOverrideFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(ContentTypeOverrideFilter.class);

    private final Set<Pattern> excludedUrls = new HashSet<>();

    class ContentTypeOverrideRequestWrapper extends HttpServletRequestWrapper {

        public ContentTypeOverrideRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public Enumeration<String> getHeaders(String name) {
            Enumeration<String> headerValues = super.getHeaders(name);
            if (HttpHeaders.CONTENT_TYPE.equals(name)) {
                Set<String> newContentTypeValues = new HashSet<>();
                while (headerValues.hasMoreElements()) {
                    String value = headerValues.nextElement();
                    if(value != null && value.startsWith(MediaType.APPLICATION_JSON)) {
                        newContentTypeValues.add(value.replace(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN));
                    } else {
                        newContentTypeValues.add(value);
                    }
                }
                return Collections.enumeration(newContentTypeValues);
            }
            return headerValues;
        }

        @Override
        public String getHeader(String name) {
            if (HttpHeaders.CONTENT_TYPE.equals(name)) {
                String header = super.getHeader(name);
                if (header != null && header.startsWith(MediaType.APPLICATION_JSON)) {
                    return header.replace(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN);
                }
            }
            return super.getHeader(name);
        }
    }

    class ContentTypeOverrideResponseWrapper extends HttpServletResponseWrapper {

        public ContentTypeOverrideResponseWrapper(HttpServletResponse response) {
            super(response);
            super.setContentType(MediaType.APPLICATION_JSON);
        }

        @Override
        public void setHeader(String name, String value) {
            if (!HttpHeaders.CONTENT_TYPE.equals(name)) {
                super.setHeader(name, value);
            }
        }

        @Override
        public void addHeader(String name, String value) {
            if (!HttpHeaders.CONTENT_TYPE.equals(name)) {
                super.addHeader(name, value);
            }
        }

        @Override
        public void setContentType(String type) {
            // Ignore changing the content type
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            String contentType = httpServletRequest.getContentType();

            if (contentType != null && contentType.startsWith(MediaType.APPLICATION_JSON) && !isUrlExcluded(httpServletRequest.getPathInfo())) {
                ContentTypeOverrideRequestWrapper requestWrapper = new ContentTypeOverrideRequestWrapper(httpServletRequest);
                ContentTypeOverrideResponseWrapper responseWrapper = new ContentTypeOverrideResponseWrapper((HttpServletResponse) response);

                chain.doFilter(requestWrapper, responseWrapper);
                return;
            }
        }

        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        try {
            ClassPath classPath = ClassPath.from(ClassLoader.getSystemClassLoader());
            ImmutableSet<ClassPath.ClassInfo> classes = classPath.getTopLevelClassesRecursive("org.apache.ambari.server.api");

            restart:
            for (ClassPath.ClassInfo classInfo: classes) {
                Class<?> clazz = classInfo.load();
                if (clazz.isAnnotationPresent(Path.class)) {
                    Path path = clazz.getAnnotation(Path.class);
                    for (Method method : clazz.getMethods()) {
                        if (method.isAnnotationPresent(Consumes.class)) {
                            Consumes consumesAnnotation = method.getAnnotation(Consumes.class);
                            for (String consume : consumesAnnotation.value()) {
                                if (MediaType.APPLICATION_JSON.equals(consume)) {
                                    excludedUrls.add(Pattern.compile(path.value()));
                                    continue restart;
                                }
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error("Failed to discover URLs that are excluded from Content-Type override. Falling back to pre-defined list of exluded URLs.", e);

            /* Do not fail here, but fallback to manual definition of excluded endpoints. */
            excludedUrls.add(Pattern.compile("/bootstrap"));
        } finally {
            excludedUrls.add(Pattern.compile("/views/.*"));
        }
    }

    private boolean isUrlExcluded(String pathInfo) {
        return excludedUrls.stream().anyMatch(p -> p.matcher(pathInfo).matches());
    }

    @Override
    public void destroy() {
    }
}
