// ***************************************************************************************************************************
// * 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.juneau.rest;

import static org.apache.juneau.rest.Enablement.*;

import java.util.*;
import java.util.logging.*;

import javax.servlet.http.*;

import org.apache.juneau.*;
import org.apache.juneau.json.*;

/**
 * Represents a set of logging rules for how to handle logging of HTTP requests/responses.
 */
public class RestCallLoggerConfig {

	/**
	 * Default empty logging config.
	 */
	public static final RestCallLoggerConfig DEFAULT = RestCallLoggerConfig.create().build();

	/**
	 * Default debug logging config.
	 */
	public static final RestCallLoggerConfig DEFAULT_DEBUG =
		RestCallLoggerConfig
			.create()
			.useStackTraceHashing(false)
			.level(Level.WARNING)
			.rules(
				RestCallLoggerRule
					.create()
					.codes("*")
					.verbose()
					.build()
			)
			.build();

	private final RestCallLoggerRule[] rules;
	private final boolean useStackTraceHashing;
	private final Enablement disabled;
	private final int stackTraceHashingTimeout;
	private final Level level;

	RestCallLoggerConfig(Builder b) {
		RestCallLoggerConfig p = b.parent;

		this.disabled = b.disabled != null ? b.disabled : p != null ? p.disabled : FALSE;
		this.useStackTraceHashing = b.useStackTraceHashing != null ? b.useStackTraceHashing : p != null ? p.useStackTraceHashing : false;
		this.stackTraceHashingTimeout = b.stackTraceHashingTimeout != null ? b.stackTraceHashingTimeout : p != null ? p.stackTraceHashingTimeout : Integer.MAX_VALUE;
		this.level = b.level != null ? b.level : p != null ? p.level : Level.INFO;

		ArrayList<RestCallLoggerRule> rules = new ArrayList<>();
		rules.addAll(b.rules);
		if (p != null)
			rules.addAll(Arrays.asList(p.rules));
		this.rules = rules.toArray(new RestCallLoggerRule[rules.size()]);
	}

	/**
	 * Creates a builder for this class.
	 *
	 * @return A new builder for this class.
	 */
	public static Builder create() {
		return new Builder();
	}

	/**
	 * Builder for {@link RestCallLoggerConfig} objects.
	 */
	public static class Builder {
		List<RestCallLoggerRule> rules = new ArrayList<>();
		RestCallLoggerConfig parent;
		Level level;
		Boolean useStackTraceHashing;
		Enablement disabled;
		Integer stackTraceHashingTimeout;

		/**
		 * Sets the parent logging config.
		 *
		 * @param parent
		 * 	The parent logging config.
		 * 	<br>Can be <jk>null</jk>.
		 * @return This object (for method chaining).
		 */
		public Builder parent(RestCallLoggerConfig parent) {
			this.parent = parent;
			return this;
		}

		/**
		 * Adds a new logging rule to this config.
		 *
		 * <p>
		 * 	The rule will be added to the END of list of current rules and thus checked last in the current list but
		 * 	before any parent rules.
		 *
		 * @param rule The logging rule to add to this config.
		 * @return This object (for method chaining).
		 */
		public Builder rule(RestCallLoggerRule rule) {
			this.rules.add(rule);
			return this;
		}

		/**
		 * Adds new logging rules to this config.
		 *
		 * <p>
		 * 	The rules will be added in order to the END of list of current rules and thus checked last in the current list but
		 * 	before any parent rules.
		 *
		 * @param rules The logging rules to add to this config.
		 * @return This object (for method chaining).
		 */
		public Builder rules(RestCallLoggerRule...rules) {
			for (RestCallLoggerRule rule : rules)
				this.rules.add(rule);
			return this;
		}

		/**
		 * Enables no-trace mode on this config.
		 *
		 * <p>
		 * No-trace mode prevents logging of messages to the log file.
		 *
		 * <p>
		 * Possible values (case-insensitive):
		 * <ul>
		 * 	<li>{@link Enablement#TRUE TRUE} - No-trace mode enabled for all requests.
		 * 	<li>{@link Enablement#FALSE FALSE} - No-trace mode disabled for all requests.
		 * 	<li>{@link Enablement#PER_REQUEST PER_REQUEST} - No-trace mode enabled for requests that have a <js>"X-NoTrace: true"</js> header.
		 * </ul>
		 *
		 * @param value
		 * 	The value for this property.
		 * 	<br>Can be <jk>null</jk> (inherit from parent or default to {@link Enablement#FALSE NEVER}).
		 * @return This object (for method chaining).
		 */
		public Builder disabled(Enablement value) {
			this.disabled = value;
			return this;
		}

		/**
		 * Shortcut for calling <c>disabled(<jsf>TRUE</jsf>)</c>.
		 *
		 * @return This object (for method chaining).
		 */
		public Builder disabled() {
			return disabled(TRUE);
		}

		/**
		 * Enables the use of stacktrace hashing.
		 *
		 * <p>
		 * When enabled, stacktraces will be replaced with hashes in the log file.
		 *
		 * @param value
		 * 	The value for this property.
		 * 	<br>Can be <jk>null</jk> (inherit from parent or default to <jk>false</jk>).
		 * @return This object (for method chaining).
		 */
		public Builder useStackTraceHashing(Boolean value) {
			this.useStackTraceHashing = value;
			return this;
		}

		/**
		 * Shortcut for calling <c>useStackTraceHashing(<jk>true</jk>);</c>.
		 *
		 * @return This object (for method chaining).
		 */
		public Builder useStackTraceHashing() {
			this.useStackTraceHashing = true;
			return this;
		}

		/**
		 * Enables a timeout after which stack traces hashes are flushed.
		 *
		 * @param timeout
		 * 	Time in milliseconds to hash stack traces for.
		 * 	<br>Can be <jk>null</jk> (inherit from parent or default to {@link Integer#MAX_VALUE MAX_VALUE}).
		 * @return This object (for method chaining).
		 */
		public Builder stackTraceHashingTimeout(Integer timeout) {
			this.stackTraceHashingTimeout = timeout;
			return this;
		}

		/**
		 * The default logging level.
		 *
		 * <p>
		 * This defines the logging level for messages if they're not already defined on the matched rule.
		 *
		 * @param value
		 * 	The value for this property.
		 * 	<br>Can be <jk>null</jk> (inherit from parent or default to {@link Level#INFO INFO}).
		 * @return This object (for method chaining).
		 */
		public Builder level(Level value) {
			this.level = value;
			return this;
		}

		/**
		 * Applies the properties in the specified object map to this builder.
		 *
		 * @param m The map containing properties to apply.
		 * @return This object (for method chaining).
		 */
		public Builder apply(ObjectMap m) {
			for (String key : m.keySet()) {
				if ("useStackTraceHashing".equals(key))
					useStackTraceHashing(m.getBoolean("useStackTraceHashing"));
				else if ("stackTraceHashingTimeout".equals(key))
					stackTraceHashingTimeout(m.getInt("stackTraceHashingTimeout"));
				else if ("disabled".equals(key))
					disabled(m.get("disabled", Enablement.class));
				else if ("rules".equals(key))
					rules(m.get("rules", RestCallLoggerRule[].class));
				else if ("level".equals(key))
					level(m.get("level", Level.class));
			}
			return this;
		}

		/**
		 * Creates the {@link RestCallLoggerConfig} object based on settings on this builder.
		 *
		 * @return A new {@link RestCallLoggerConfig} object.
		 */
		public RestCallLoggerConfig build() {
			return new RestCallLoggerConfig(this);
		}
	}

	/**
	 * Given the specified servlet request/response, find the rule that applies to it.
	 *
	 * @param req The servlet request.
	 * @param res The servlet response.
	 * @return The applicable logging rule, or <jk>null<jk> if a match could not be found.
	 */
	public RestCallLoggerRule getRule(HttpServletRequest req, HttpServletResponse res) {

		int status = res.getStatus();
		Throwable e = (Throwable)req.getAttribute("Exception");
		boolean debug = isDebug(req);

		for (RestCallLoggerRule r : rules) {
			if (r.matches(status, debug, e)) {
				Enablement disabled = r.getDisabled();
				if (disabled == null)
					disabled = this.disabled;
				if (disabled == TRUE)
					return null;
				if (isNoTraceAttr(req))
					return null;
				if (disabled == FALSE)
					return r;
				if (isNoTraceHeader(req))
					return null;
				return r;
			}
		}

		return null;
	}

	/**
	 * Returns <jk>true</jk> if logging is disabled for this request.
	 *
	 * @param req The HTTP request.
	 * @return <jk>true</jk> if logging is disabled for this request.
	 */
	public boolean isDisabled(HttpServletRequest req) {
		if (disabled == TRUE)
			return true;
		if (disabled == FALSE)
			return false;
		return isNoTraceAttr(req);
	}

	private boolean isDebug(HttpServletRequest req) {
		Boolean b = boolAttr(req, "Debug");
		return (b != null && b == true);
	}

	private boolean isNoTraceAttr(HttpServletRequest req) {
		Boolean b = boolAttr(req, "NoTrace");
		return (b != null && b == true);
	}

	private boolean isNoTraceHeader(HttpServletRequest req) {
		return "true".equalsIgnoreCase(req.getHeader("X-NoTrace"));
	}

	/**
	 * Returns the default logging level.
	 *
	 * @return The default logging level.
	 */
	public Level getLevel() {
		return level;
	}

	/**
	 * Returns <jk>true</jk> if stack traces should be hashed.
	 *
	 * @return <jk>true</jk> if stack traces should be hashed.
	 */
	public boolean isUseStackTraceHashing() {
		return useStackTraceHashing;
	}

	/**
	 * Returns the time in milliseconds that stacktrace hashes should be persisted.
	 *
	 * @return The time in milliseconds that stacktrace hashes should be persisted.
	 */
	public int getStackTraceHashingTimeout() {
		return stackTraceHashingTimeout;
	}

	/**
	 * Returns the rules defined in this config.
	 *
	 * @return Thew rules defined in this config.
	 */
	public List<RestCallLoggerRule> getRules() {
		return Collections.unmodifiableList(Arrays.asList(rules));
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Other methods
	//-----------------------------------------------------------------------------------------------------------------

	private Boolean boolAttr(HttpServletRequest req, String name) {
		Object o = req.getAttribute(name);
		if (o == null || ! (o instanceof Boolean))
			return null;
		return (Boolean)o;
	}

	@Override /* Object */
	public String toString() {
		return SimpleJsonSerializer.DEFAULT_READABLE.toString(toMap());
	}

	/**
	 * Returns the properties defined on this bean context as a simple map for debugging purposes.
	 *
	 * @return A new map containing the properties defined on this context.
	 */
	public ObjectMap toMap() {
		return new DefaultFilteringObjectMap()
			.append("useStackTraceHashing", useStackTraceHashing)
			.append("disabled", disabled == FALSE ? null : disabled)
			.append("stackTraceHashingTimeout", stackTraceHashingTimeout == Integer.MAX_VALUE ? null : stackTraceHashingTimeout)
			.append("level", level == Level.INFO ? null : level)
			.append("rules", rules.length == 0 ? null : rules)
		;
	}
}
