/*
 * 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.authentication.strategy;

import org.apache.wicket.Application;
import org.apache.wicket.authentication.IAuthenticationStrategy;
import org.apache.wicket.util.cookies.CookieDefaults;
import org.apache.wicket.util.cookies.CookieUtils;
import org.apache.wicket.util.crypt.CachingSunJceCryptFactory;
import org.apache.wicket.util.crypt.ICrypt;
import org.apache.wicket.util.lang.Args;
import org.apache.wicket.util.string.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Wicket's default implementation of an authentication strategy. It'll concatenate username and
 * password, encrypt it and put it into one Cookie.
 * 
 * @author Juergen Donnerstag
 */
public class DefaultAuthenticationStrategy implements IAuthenticationStrategy
{
	private static final Logger logger = LoggerFactory.getLogger(DefaultAuthenticationStrategy.class);

	/** The cookie name to store the username and password */
	protected final String cookieKey;

	/** The key to use for encrypting/decrypting the cookie value  */
	protected final String encryptionKey;

	/** The separator used to concatenate the username and password */
	protected final String VALUE_SEPARATOR = "-sep-";

	/** Cookie utils with default settings */
	private CookieUtils cookieUtils;

	/** Use to encrypt cookie values for username and password. */
	private ICrypt crypt;

	/**
	 * Constructor
	 * 
	 * @param cookieKey
	 *            The name of the cookie
	 */
	public DefaultAuthenticationStrategy(final String cookieKey)
	{
		this(cookieKey, defaultEncryptionKey(cookieKey));
	}

	private static String defaultEncryptionKey(String cookieKey)
	{
		if (Application.exists())
		{
			return Application.get().getName();
		}
		return cookieKey;
	}

	public DefaultAuthenticationStrategy(final String cookieKey, final String encryptionKey)
	{
		this.cookieKey = Args.notEmpty(cookieKey, "cookieKey");
		this.encryptionKey = Args.notEmpty(encryptionKey, "encryptionKey");
	}

	/**
	 * Make sure you always return a valid CookieUtils
	 * 
	 * @return CookieUtils
	 */
	protected CookieUtils getCookieUtils()
	{
		if (cookieUtils == null)
		{
			CookieDefaults settings = new CookieDefaults();
			settings.setHttpOnly(true);
			cookieUtils = new CookieUtils(settings);
		}
		return cookieUtils;
	}

	/**
	 * @return The crypt engine to be used
	 */
	protected ICrypt getCrypt()
	{
		if (crypt == null)
		{
			CachingSunJceCryptFactory cryptFactory = new CachingSunJceCryptFactory(encryptionKey);
			crypt = cryptFactory.newCrypt();
		}
		return crypt;
	}

	@Override
	public String[] load()
	{
		String value = getCookieUtils().load(cookieKey);
		if (Strings.isEmpty(value) == false)
		{
			try
			{
				value = getCrypt().decryptUrlSafe(value);
			}
			catch (RuntimeException e)
			{
				logger.info(
					"Error decrypting login cookie: {}. The cookie will be deleted. Possible cause is that a session-relative encryption key was used to encrypt this cookie while this decryption attempt is happening in a different session, eg user coming back to the application after session expiration",
					cookieKey);
				getCookieUtils().remove(cookieKey);
				value = null;
			}
			return decode(value);
		}

		return null;
	}

	/**
	 * This method will decode decrypted cookie value based on application needs
	 *
	 * @param value decrypted cookie value
	 * @return decomposed values array, or null in case cookie value was empty.
	 */
	protected String[] decode(String value) {
		if (Strings.isEmpty(value) == false)
		{
			String username = null;
			String password = null;

			String[] values = value.split(VALUE_SEPARATOR);
			if ((values.length > 0) && (Strings.isEmpty(values[0]) == false))
			{
				username = values[0];
			}
			if ((values.length > 1) && (Strings.isEmpty(values[1]) == false))
			{
				password = values[1];
			}

			return new String[] { username, password };
		}
		return null;
	}

	@Override
	public void save(final String credential, final String... extraCredentials)
	{
		String encryptedValue = getCrypt().encryptUrlSafe(encode(credential, extraCredentials));

		getCookieUtils().save(cookieKey, encryptedValue);
	}

	/**
	 * This method can be overridden to provide different encoding mechanism
	 *
	 * @param credential
	 * @param extraCredentials
	 * @return String representation of the parameters given
	 */
	protected String encode(final String credential, final String... extraCredentials)
	{
		StringBuilder value = new StringBuilder(credential);
		if (extraCredentials != null)
		{
			for (String extraCredential : extraCredentials)
			{
				value.append(VALUE_SEPARATOR).append(extraCredential);
			}
		}
		return value.toString();
	}

	@Override
	public void remove()
	{
		getCookieUtils().remove(cookieKey);
	}
}
