/*
 * Copyright 2004-present the original author or authors.
 *
 * 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
 *
 *      https://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.springframework.security.web;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;

import org.springframework.http.HttpHeaders;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Holds objects associated with a HTTP filter.
 * <P>
 * Guarantees the request and response are instances of <code>HttpServletRequest</code>
 * and <code>HttpServletResponse</code>, and that there are no <code>null</code> objects.
 * <p>
 * Required so that security system classes can obtain access to the filter environment,
 * as well as the request and response.
 *
 * @author Ben Alex
 * @author colin sampaleanu
 * @author Luke Taylor
 * @author Rob Winch
 */
public class FilterInvocation {

	static final FilterChain DUMMY_CHAIN = (req, res) -> {
		throw new UnsupportedOperationException("Dummy filter chain");
	};

	private final FilterChain chain;

	private HttpServletRequest request;

	private @Nullable HttpServletResponse response;

	public FilterInvocation(ServletRequest request, ServletResponse response, FilterChain chain) {
		Assert.isTrue(request != null && response != null && chain != null, "Cannot pass null values to constructor");
		this.request = (HttpServletRequest) request;
		this.response = (HttpServletResponse) response;
		this.chain = chain;
	}

	public FilterInvocation(String servletPath, String method) {
		this(null, servletPath, method);
	}

	public FilterInvocation(@Nullable String contextPath, String servletPath, String method) {
		this(contextPath, servletPath, method, null);
	}

	public FilterInvocation(@Nullable String contextPath, String servletPath, @Nullable String method,
			@Nullable ServletContext servletContext) {
		this(contextPath, servletPath, null, null, method, servletContext);
	}

	public FilterInvocation(String contextPath, String servletPath, String pathInfo, String query, String method) {
		this(contextPath, servletPath, pathInfo, query, method, null);
	}

	public FilterInvocation(@Nullable String contextPath, String servletPath, @Nullable String pathInfo,
			@Nullable String query, @Nullable String method, @Nullable ServletContext servletContext) {
		DummyRequest request = new DummyRequest();
		contextPath = (contextPath != null) ? contextPath : "/cp";
		request.setContextPath(contextPath);
		request.setServletPath(servletPath);
		request.setRequestURI(contextPath + servletPath + ((pathInfo != null) ? pathInfo : ""));
		request.setPathInfo(pathInfo);
		request.setQueryString(query);
		request.setMethod(method);
		request.setServletContext(servletContext);
		this.request = request;
		this.chain = DUMMY_CHAIN;
	}

	public FilterChain getChain() {
		return this.chain;
	}

	/**
	 * Indicates the URL that the user agent used for this request.
	 * <p>
	 * @return the full URL of this request
	 */
	public String getFullRequestUrl() {
		return UrlUtils.buildFullRequestUrl(this.request);
	}

	public HttpServletRequest getHttpRequest() {
		return this.request;
	}

	public @Nullable HttpServletResponse getHttpResponse() {
		return this.response;
	}

	/**
	 * Obtains the web application-specific fragment of the URL.
	 * @return the URL, excluding any server name, context path or servlet path
	 */
	public String getRequestUrl() {
		return UrlUtils.buildRequestUrl(this.request);
	}

	public HttpServletRequest getRequest() {
		return getHttpRequest();
	}

	public @Nullable HttpServletResponse getResponse() {
		return getHttpResponse();
	}

	@Override
	public String toString() {
		if (!StringUtils.hasLength(this.request.getMethod())) {
			return "filter invocation [" + getRequestUrl() + "]";
		}
		else {
			return "filter invocation [" + this.request.getMethod() + " " + getRequestUrl() + "]";
		}
	}

	static class DummyRequest extends HttpServletRequestWrapper {

		private static final HttpServletRequest UNSUPPORTED_REQUEST = (HttpServletRequest) Proxy.newProxyInstance(
				DummyRequest.class.getClassLoader(), new Class[] { HttpServletRequest.class },
				new UnsupportedOperationExceptionInvocationHandler());

		private @Nullable String requestURI;

		private String contextPath = "";

		private @Nullable String servletPath;

		private @Nullable String pathInfo;

		private @Nullable String queryString;

		private @Nullable String method;

		private @Nullable ServletContext servletContext;

		private final HttpHeaders headers = new HttpHeaders();

		private final Map<String, String[]> parameters = new LinkedHashMap<>();

		DummyRequest() {
			super(UNSUPPORTED_REQUEST);
		}

		@Override
		public String getCharacterEncoding() {
			return "UTF-8";
		}

		@Override
		public @Nullable Object getAttribute(String attributeName) {
			return null;
		}

		void setRequestURI(String requestURI) {
			this.requestURI = requestURI;
		}

		void setPathInfo(@Nullable String pathInfo) {
			this.pathInfo = pathInfo;
		}

		@Override
		public @Nullable String getRequestURI() {
			return this.requestURI;
		}

		void setContextPath(String contextPath) {
			this.contextPath = contextPath;
		}

		@Override
		public String getContextPath() {
			return this.contextPath;
		}

		void setServletPath(String servletPath) {
			this.servletPath = servletPath;
		}

		@Override
		public @Nullable String getServletPath() {
			return this.servletPath;
		}

		void setMethod(@Nullable String method) {
			this.method = method;
		}

		@Override
		public @Nullable String getMethod() {
			return this.method;
		}

		@Override
		public @Nullable String getPathInfo() {
			return this.pathInfo;
		}

		@Override
		public @Nullable String getQueryString() {
			return this.queryString;
		}

		void setQueryString(@Nullable String queryString) {
			this.queryString = queryString;
		}

		@Override
		public @Nullable String getServerName() {
			return null;
		}

		@Override
		public @Nullable String getHeader(String name) {
			return this.headers.getFirst(name);
		}

		@Override
		public Enumeration<String> getHeaders(String name) {
			List<String> headerList = this.headers.get(name);
			if (headerList == null) {
				return Collections.emptyEnumeration();
			}
			return Collections.enumeration(headerList);
		}

		@Override
		public Enumeration<String> getHeaderNames() {
			return Collections.enumeration(this.headers.headerNames());
		}

		@Override
		public int getIntHeader(String name) {
			String value = this.headers.getFirst(name);
			if (value == null) {
				return -1;
			}
			return Integer.parseInt(value);
		}

		void addHeader(String name, String value) {
			this.headers.add(name, value);
		}

		@Override
		public @Nullable String getParameter(String name) {
			String[] array = this.parameters.get(name);
			return (array != null && array.length > 0) ? array[0] : null;
		}

		@Override
		public Map<String, String[]> getParameterMap() {
			return Collections.unmodifiableMap(this.parameters);
		}

		@Override
		public Enumeration<String> getParameterNames() {
			return Collections.enumeration(this.parameters.keySet());
		}

		@Override
		public String @Nullable [] getParameterValues(String name) {
			return this.parameters.get(name);
		}

		void setParameter(String name, String... values) {
			this.parameters.put(name, values);
		}

		@Override
		public @Nullable ServletContext getServletContext() {
			return this.servletContext;
		}

		void setServletContext(@Nullable ServletContext servletContext) {
			this.servletContext = servletContext;
		}

	}

	static final class UnsupportedOperationExceptionInvocationHandler implements InvocationHandler {

		private static final float JAVA_VERSION = Float.parseFloat(System.getProperty("java.class.version", "52"));

		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			if (method.isDefault()) {
				return invokeDefaultMethod(proxy, method, args);
			}
			throw new UnsupportedOperationException(method + " is not supported");
		}

		private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable {
			if (isJdk8OrEarlier()) {
				return invokeDefaultMethodForJdk8(proxy, method, args);
			}
			return MethodHandles.lookup()
				.findSpecial(method.getDeclaringClass(), method.getName(),
						MethodType.methodType(method.getReturnType(), new Class[0]), method.getDeclaringClass())
				.bindTo(proxy)
				.invokeWithArguments(args);
		}

		private Object invokeDefaultMethodForJdk8(Object proxy, Method method, Object[] args) throws Throwable {
			Constructor<Lookup> constructor = Lookup.class.getDeclaredConstructor(Class.class);
			constructor.setAccessible(true);
			Class<?> clazz = method.getDeclaringClass();
			return constructor.newInstance(clazz)
				.in(clazz)
				.unreflectSpecial(method, clazz)
				.bindTo(proxy)
				.invokeWithArguments(args);
		}

		private boolean isJdk8OrEarlier() {
			return JAVA_VERSION <= 52;
		}

	}

}
