/*
 * 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.wicket.util.cookies;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;

import jakarta.servlet.ServletContext;
import jakarta.servlet.http.Cookie;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Helper class to simplify Cookie handling.
 *
 * @author Juergen Donnerstag
 * @author Jonathan Locke
 */
public class CookieUtils
{
	private final static Logger log = LoggerFactory.getLogger(CookieUtils.class);

	public static final String DEFAULT_SESSIONID_COOKIE_NAME = "JSESSIONID";

	private final CookieDefaults settings;

	/**
	 * Construct.
	 */
	public CookieUtils()
	{
		settings = new CookieDefaults();
	}

	/**
	 * Construct.
	 *
	 * @param settings
	 *          the default settings for the saved cookies
	 */
	public CookieUtils(final CookieDefaults settings)
	{
		this.settings = settings;
	}

	/**
	 * @return Gets the settings for these utils
	 */
	public final CookieDefaults getSettings()
	{
		return settings;
	}

	/**
	 * Remove the cookie identified by the key
	 *
	 * @param key
	 *          The cookie name
	 */
	public final void remove(final String key)
	{
		final Cookie cookie = getCookie(key);
		if (cookie != null)
		{
			remove(cookie);
		}
	}

	/**
	 * Remove the cookie identified by the form component
	 *
	 * @param formComponent
	 */
	public final void remove(final FormComponent<?> formComponent)
	{
		remove(getKey(formComponent));
	}

	/**
	 * This method gets used when a cookie key needs to be derived from a form component. By default
	 * the component's page relative path is used.
	 *
	 * @param component
	 * @return cookie key
	 */
	protected String getKey(final FormComponent<?> component)
	{
		return getSaveKey(component.getPageRelativePath());
	}

	/**
	 * Retrieve the cookie value by means of its key.
	 *
	 * @param key
	 *          The cookie name
	 * @return The cookie value associated with the key
	 */
	public final String load(final String key)
	{
		final Cookie cookie = getCookie(key);
		if (cookie != null)
		{
			return cookie.getValue();
		}
		return null;
	}

	/**
	 * Retrieve the cookie value associated with the formComponent and load the model object with
	 * the cookie value.
	 *
	 * @param formComponent
	 * @return The Cookie value which has also been used to set the component's model value
	 */
	public final String load(final FormComponent<?> formComponent)
	{
		String value = load(getKey(formComponent));
		if (value != null)
		{
			// Assign the retrieved/persisted value to the component
			formComponent.setModelValue(new String[] {value});
		}
		return value;
	}

	/**
	 * Create a Cookie with key and value and save it in the browser with the next response
	 *
	 * @param name
	 *          The cookie name
	 * @param value
	 *          The cookie value
	 */
	public final void save(String name, final String value)
	{
		Cookie cookie = getCookie(name);
		if (cookie == null)
		{
			cookie = new Cookie(name, value);
		}
		else
		{
			cookie.setValue(value);
		}
		save(cookie);
	}

	/**
	 * Save the form components model value in a cookie
	 *
	 * @param formComponent
	 */
	public final void save(final FormComponent<?> formComponent)
	{
		save(getKey(formComponent), formComponent.getValue());
	}

	/**
	 * Make sure the 'key' does not contain any illegal chars. E.g. for cookies ':' is not allowed.
	 *
	 * @param key
	 *            The key to be validated
	 * @return The save key
	 */
	protected String getSaveKey(String key)
	{
		if (Strings.isEmpty(key))
		{
			throw new IllegalArgumentException("A Cookie name can not be null or empty");
		}

		// cookie names cannot contain ':',
		// we replace ':' with '.' but first we have to encode '.' as '..'
		key = Strings.replaceAll(key, ".", "..").toString();
		key = key.replace(':', '.');
		return key;
	}

	/**
	 * Convenience method for deleting a cookie by name. Delete the cookie by setting its maximum
	 * age to zero.
	 *
	 * @param cookie
	 *            The cookie to delete
	 */
	private void remove(final Cookie cookie)
	{
		if (cookie != null)
		{
			save(cookie);

			// Delete the cookie by setting its maximum age to zero
			cookie.setMaxAge(0);
			cookie.setValue(null);

			if (log.isDebugEnabled())
			{
				log.debug("Removed Cookie: " + cookie.getName());
			}
		}
	}

	/**
	 * Gets the cookie with 'name' attached to the latest WebRequest.
	 *
	 * @param name
	 *            The name of the cookie to be looked up
	 *
	 * @return Any cookies for this request
	 */
	public Cookie getCookie(final String name)
	{
		try
		{
			WebRequest webRequest = getWebRequest();
			Cookie cookie = webRequest.getCookie(name);
			if (log.isDebugEnabled())
			{
				if (cookie != null)
				{
					log.debug("Found Cookie with name=" + name + " and request URI=" +
							webRequest.getUrl().toString());
				}
				else
				{
					log.debug("Unable to find Cookie with name=" + name + " and request URI=" +
							webRequest.getUrl().toString());
				}
			}

			return cookie;
		}
		catch (NullPointerException ex)
		{
			// Ignore any app server problem here
		}

		return null;
	}


	/**
	 * Gets the name of the cookie where the session id is stored.
	 *
	 * @param application
	 *            The current we application holding the {@link jakarta.servlet.ServletContext}.
	 *
	 * @return The name set in {@link jakarta.servlet.SessionCookieConfig} or the default value 'JSESSIONID' if not set
	 */
	public String getSessionIdCookieName(WebApplication application)
	{
		String jsessionCookieName = application.getServletContext().getSessionCookieConfig().getName();

		return jsessionCookieName == null ? DEFAULT_SESSIONID_COOKIE_NAME : jsessionCookieName;
	}

	/**
	 * Persist/save the data using Cookies.
	 *
	 * @param cookie
	 *            The Cookie to be persisted.
	 * @return The cookie provided
	 */
	private Cookie save(final Cookie cookie)
	{
		if (cookie == null)
		{
			return null;
		}

		initializeCookie(cookie);

		getWebResponse().addCookie(cookie);

		if (log.isDebugEnabled())
		{
			log.debug("Cookie saved: " + cookieToDebugString(cookie) + "; request URI=" +
				getWebRequest().getUrl().toString());
		}

		return cookie;
	}

	/**
	 * Is called before the Cookie is saved. May be subclassed for different (dynamic) Cookie
	 * parameters. Static parameters can also be changed via {@link CookieDefaults}.
	 *
	 * @param cookie
	 */
	protected void initializeCookie(final Cookie cookie)
	{
		final String comment = settings.getComment();
		if (comment != null)
		{
			cookie.setComment(comment);
		}

		final String domain = settings.getDomain();
		if (domain != null)
		{
			cookie.setDomain(domain);
		}

		ServletWebRequest request = (ServletWebRequest)getWebRequest();
		String path = request.getContainerRequest().getContextPath() + "/" +
			request.getFilterPrefix();

		if (settings.getSameSite() == CookieDefaults.SameSite.None)
		{
			settings.setSecure(true);
		}

		cookie.setPath(path);
		cookie.setVersion(settings.getVersion());
		cookie.setSecure(settings.getSecure());
		cookie.setMaxAge(settings.getMaxAge());
		cookie.setHttpOnly(settings.isHttpOnly());

		setAttribute(cookie, "SameSite", settings.getSameSite().name());
	}

	/**
	 * Sets a custom attribute on Servlet 6+
	 * 
	 * @param cookie
	 * 		The cookie to set the attribute on
	 * @param attributeName
	 * 		The name of the attribute
	 * @param attributeValue
	 * 		The value of the attribute
	 */
	public static void setAttribute(final Cookie cookie, String attributeName, String attributeValue)
	{
		Args.notEmpty(attributeName, "attributeName");

		if (WebApplication.exists())
		{
			final ServletContext servletContext = WebApplication.get().getServletContext();
			if (servletContext.getEffectiveMajorVersion() >= 6)
			{
				cookie.setAttribute(attributeName, attributeValue);
			}
		}
	}

	/**
	 * Convenience method to get the http request.
	 *
	 * @return WebRequest related to the RequestCycle
	 */
	private WebRequest getWebRequest()
	{
		return (WebRequest)RequestCycle.get().getRequest();
	}

	/**
	 * Convenience method to get the http response.
	 *
	 * @return WebResponse related to the RequestCycle
	 */
	private WebResponse getWebResponse()
	{
		RequestCycle cycle = RequestCycle.get();
		Response response = cycle.getResponse();
		if (!(response instanceof WebResponse))
		{
			response = cycle.getOriginalResponse();
		}
		return (WebResponse)response;
	}

	/**
	 * Gets debug info as a string for the given cookie.
	 *
	 * @param cookie
	 *            the cookie to debug.
	 * @return a string that represents the internals of the cookie.
	 */
	private String cookieToDebugString(final Cookie cookie)
	{
		final LocalDateTime localDateTime = Instant.ofEpochMilli(cookie.getMaxAge()).atZone(ZoneId.systemDefault()).toLocalDateTime();
		return "[Cookie " + " name = " + cookie.getName() + ", value = " + cookie.getValue() +
			", domain = " + cookie.getDomain() + ", path = " + cookie.getPath() + ", maxAge = " +
			localDateTime + "(" + cookie.getMaxAge() + ")" + "]";
	}
}
